Refactor dashboard JS to ES modules; fix collector strategy loading and add auto-backfill
- Split inline JS into separate ES module files (indicators/, strategies/, ui/, utils/) - Fix brain.py strategy registry to use MAStrategy directly instead of missing modules - Add auto-backfill for detected data gaps in collector monitoring loop - Fix chart resize on sidebar toggle - Fix chart scrollToTime -> setVisibleLogicalRange
This commit is contained in:
File diff suppressed because it is too large
Load Diff
88
src/api/dashboard/static/js/app.js
Normal file
88
src/api/dashboard/static/js/app.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
|
||||
import { restoreSidebarState, toggleSidebar } from './ui/sidebar.js';
|
||||
import { SimulationStorage } from './ui/storage.js';
|
||||
import { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './ui/export.js';
|
||||
import {
|
||||
runSimulation,
|
||||
displayEnhancedResults,
|
||||
showSimulationMarkers,
|
||||
clearSimulationResults,
|
||||
getLastResults,
|
||||
setLastResults
|
||||
} from './ui/simulation.js';
|
||||
import {
|
||||
renderStrategies,
|
||||
selectStrategy,
|
||||
loadStrategies,
|
||||
saveSimulation,
|
||||
renderSavedSimulations,
|
||||
loadSavedSimulation,
|
||||
deleteSavedSimulation,
|
||||
setCurrentStrategy
|
||||
} from './ui/strategies-panel.js';
|
||||
import {
|
||||
renderIndicatorList,
|
||||
addNewIndicator,
|
||||
selectIndicator,
|
||||
applyIndicatorConfig,
|
||||
removeIndicator,
|
||||
removeIndicatorByIndex,
|
||||
drawIndicatorsOnChart
|
||||
} from './ui/indicators-panel.js';
|
||||
import { StrategyParams } from './strategies/config.js';
|
||||
import { IndicatorRegistry } from './indicators/index.js';
|
||||
|
||||
window.dashboard = null;
|
||||
|
||||
function setDefaultStartDate() {
|
||||
const startDateInput = document.getElementById('simStartDate');
|
||||
if (startDateInput) {
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
startDateInput.value = sevenDaysAgo.toISOString().slice(0, 16);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimeframeDisplay() {
|
||||
const display = document.getElementById('simTimeframeDisplay');
|
||||
if (display && window.dashboard) {
|
||||
display.value = window.dashboard.currentInterval.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleSidebar = toggleSidebar;
|
||||
window.refreshTA = refreshTA;
|
||||
window.openAIAnalysis = openAIAnalysis;
|
||||
window.showExportDialog = showExportDialog;
|
||||
window.closeExportDialog = closeExportDialog;
|
||||
window.performExport = performExport;
|
||||
window.exportSavedSimulation = exportSavedSimulation;
|
||||
window.runSimulation = runSimulation;
|
||||
window.saveSimulation = saveSimulation;
|
||||
window.renderSavedSimulations = renderSavedSimulations;
|
||||
window.loadSavedSimulation = loadSavedSimulation;
|
||||
window.deleteSavedSimulation = deleteSavedSimulation;
|
||||
window.clearSimulationResults = clearSimulationResults;
|
||||
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
||||
|
||||
window.StrategyParams = StrategyParams;
|
||||
window.SimulationStorage = SimulationStorage;
|
||||
window.IndicatorRegistry = IndicatorRegistry;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
window.dashboard = new TradingDashboard();
|
||||
restoreSidebarState();
|
||||
setDefaultStartDate();
|
||||
updateTimeframeDisplay();
|
||||
renderSavedSimulations();
|
||||
|
||||
await loadStrategies();
|
||||
|
||||
renderIndicatorList();
|
||||
|
||||
const originalSwitchTimeframe = window.dashboard.switchTimeframe.bind(window.dashboard);
|
||||
window.dashboard.switchTimeframe = function(interval) {
|
||||
originalSwitchTimeframe(interval);
|
||||
setTimeout(() => drawIndicatorsOnChart(), 500);
|
||||
};
|
||||
});
|
||||
15
src/api/dashboard/static/js/core/constants.js
Normal file
15
src/api/dashboard/static/js/core/constants.js
Normal file
@ -0,0 +1,15 @@
|
||||
export const INTERVALS = ['1m', '3m', '5m', '15m', '30m', '37m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M'];
|
||||
|
||||
export const COLORS = {
|
||||
tvBg: '#131722',
|
||||
tvPanelBg: '#1e222d',
|
||||
tvBorder: '#2a2e39',
|
||||
tvText: '#d1d4dc',
|
||||
tvTextSecondary: '#787b86',
|
||||
tvGreen: '#26a69a',
|
||||
tvRed: '#ef5350',
|
||||
tvBlue: '#2962ff',
|
||||
tvHover: '#2a2e39'
|
||||
};
|
||||
|
||||
export const API_BASE = '/api/v1';
|
||||
1
src/api/dashboard/static/js/core/index.js
Normal file
1
src/api/dashboard/static/js/core/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { INTERVALS, COLORS, API_BASE } from './constants.js';
|
||||
36
src/api/dashboard/static/js/indicators/atr.js
Normal file
36
src/api/dashboard/static/js/indicators/atr.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class ATRIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 14;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
const tr = new Array(candles.length).fill(0);
|
||||
|
||||
for (let i = 1; i < candles.length; i++) {
|
||||
const h_l = candles[i].high - candles[i].low;
|
||||
const h_pc = Math.abs(candles[i].high - candles[i-1].close);
|
||||
const l_pc = Math.abs(candles[i].low - candles[i-1].close);
|
||||
tr[i] = Math.max(h_l, h_pc, l_pc);
|
||||
}
|
||||
|
||||
let atr = 0;
|
||||
let sum = 0;
|
||||
for (let i = 1; i <= period; i++) sum += tr[i];
|
||||
atr = sum / period;
|
||||
results[period] = atr;
|
||||
|
||||
for (let i = period + 1; i < candles.length; i++) {
|
||||
atr = (atr * (period - 1) + tr[i]) / period;
|
||||
results[i] = atr;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'ATR',
|
||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
||||
plots: [{ id: 'value', color: '#795548', title: 'ATR' }]
|
||||
};
|
||||
}
|
||||
}
|
||||
17
src/api/dashboard/static/js/indicators/base.js
Normal file
17
src/api/dashboard/static/js/indicators/base.js
Normal file
@ -0,0 +1,17 @@
|
||||
export class BaseIndicator {
|
||||
constructor(config) {
|
||||
this.name = config.name;
|
||||
this.type = config.type;
|
||||
this.params = config.params || {};
|
||||
this.timeframe = config.timeframe || '1m';
|
||||
}
|
||||
calculate(candles) { throw new Error("Not implemented"); }
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: this.name,
|
||||
inputs: [],
|
||||
plots: []
|
||||
};
|
||||
}
|
||||
}
|
||||
41
src/api/dashboard/static/js/indicators/bb.js
Normal file
41
src/api/dashboard/static/js/indicators/bb.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class BollingerBandsIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 20;
|
||||
const stdDevMult = this.params.stdDev || 2;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) sum += candles[i-j].close;
|
||||
const sma = sum / period;
|
||||
|
||||
let diffSum = 0;
|
||||
for (let j = 0; j < period; j++) diffSum += Math.pow(candles[i-j].close - sma, 2);
|
||||
const stdDev = Math.sqrt(diffSum / period);
|
||||
|
||||
results[i] = {
|
||||
middle: sma,
|
||||
upper: sma + (stdDevMult * stdDev),
|
||||
lower: sma - (stdDevMult * stdDev)
|
||||
};
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Bollinger Bands',
|
||||
inputs: [
|
||||
{ name: 'period', label: 'Period', type: 'number', default: 20, min: 1, max: 100 },
|
||||
{ name: 'stdDev', label: 'Std Dev', type: 'number', default: 2, min: 0.5, max: 5, step: 0.5 }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'upper', color: '#4caf50', title: 'Upper' },
|
||||
{ id: 'middle', color: '#4caf50', title: 'Middle', lineStyle: 2 },
|
||||
{ id: 'lower', color: '#4caf50', title: 'Lower' }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
17
src/api/dashboard/static/js/indicators/ema.js
Normal file
17
src/api/dashboard/static/js/indicators/ema.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class EMAIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 44;
|
||||
return MA.ema(candles, period, 'close');
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'EMA',
|
||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
|
||||
plots: [{ id: 'value', color: '#ff9800', title: 'EMA' }]
|
||||
};
|
||||
}
|
||||
}
|
||||
40
src/api/dashboard/static/js/indicators/hts.js
Normal file
40
src/api/dashboard/static/js/indicators/hts.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class HTSIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const shortPeriod = this.params.short || 33;
|
||||
const longPeriod = this.params.long || 144;
|
||||
const maType = this.params.maType || 'RMA';
|
||||
|
||||
const shortHigh = MA.get(maType, candles, shortPeriod, 'high');
|
||||
const shortLow = MA.get(maType, candles, shortPeriod, 'low');
|
||||
const longHigh = MA.get(maType, candles, longPeriod, 'high');
|
||||
const longLow = MA.get(maType, candles, longPeriod, 'low');
|
||||
|
||||
return candles.map((_, i) => ({
|
||||
fastHigh: shortHigh[i],
|
||||
fastLow: shortLow[i],
|
||||
slowHigh: longHigh[i],
|
||||
slowLow: longLow[i]
|
||||
}));
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'HTS Trend System',
|
||||
description: 'High/Low Trend System with Fast and Slow MAs',
|
||||
inputs: [
|
||||
{ name: 'short', label: 'Fast Period', type: 'number', default: 33, min: 1, max: 500 },
|
||||
{ name: 'long', label: 'Slow Period', type: 'number', default: 144, min: 1, max: 500 },
|
||||
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'RMA' }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'fastHigh', color: '#00bcd4', title: 'Fast High', width: 1 },
|
||||
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: 1 },
|
||||
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 },
|
||||
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: 2 }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
30
src/api/dashboard/static/js/indicators/index.js
Normal file
30
src/api/dashboard/static/js/indicators/index.js
Normal file
@ -0,0 +1,30 @@
|
||||
export { MA } from './ma.js';
|
||||
export { BaseIndicator } from './base.js';
|
||||
export { HTSIndicator } from './hts.js';
|
||||
export { SMAIndicator } from './sma.js';
|
||||
export { EMAIndicator } from './ema.js';
|
||||
export { RSIIndicator } from './rsi.js';
|
||||
export { BollingerBandsIndicator } from './bb.js';
|
||||
export { MACDIndicator } from './macd.js';
|
||||
export { StochasticIndicator } from './stoch.js';
|
||||
export { ATRIndicator } from './atr.js';
|
||||
|
||||
import { HTSIndicator } from './hts.js';
|
||||
import { SMAIndicator } from './sma.js';
|
||||
import { EMAIndicator } from './ema.js';
|
||||
import { RSIIndicator } from './rsi.js';
|
||||
import { BollingerBandsIndicator } from './bb.js';
|
||||
import { MACDIndicator } from './macd.js';
|
||||
import { StochasticIndicator } from './stoch.js';
|
||||
import { ATRIndicator } from './atr.js';
|
||||
|
||||
export const IndicatorRegistry = {
|
||||
hts: HTSIndicator,
|
||||
sma: SMAIndicator,
|
||||
ema: EMAIndicator,
|
||||
rsi: RSIIndicator,
|
||||
bb: BollingerBandsIndicator,
|
||||
macd: MACDIndicator,
|
||||
stoch: StochasticIndicator,
|
||||
atr: ATRIndicator
|
||||
};
|
||||
93
src/api/dashboard/static/js/indicators/ma.js
Normal file
93
src/api/dashboard/static/js/indicators/ma.js
Normal file
@ -0,0 +1,93 @@
|
||||
export class MA {
|
||||
static get(type, candles, period, source = 'close') {
|
||||
switch (type.toUpperCase()) {
|
||||
case 'SMA': return MA.sma(candles, period, source);
|
||||
case 'EMA': return MA.ema(candles, period, source);
|
||||
case 'RMA': return MA.rma(candles, period, source);
|
||||
case 'WMA': return MA.wma(candles, period, source);
|
||||
case 'VWMA': return MA.vwma(candles, period, source);
|
||||
default: return MA.sma(candles, period, source);
|
||||
}
|
||||
}
|
||||
|
||||
static sma(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
sum += candles[i][source];
|
||||
if (i >= period) sum -= candles[i - period][source];
|
||||
if (i >= period - 1) results[i] = sum / period;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static ema(candles, period, source = 'close') {
|
||||
const multiplier = 2 / (period + 1);
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let ema = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
if (i < period) {
|
||||
sum += candles[i][source];
|
||||
if (i === period - 1) {
|
||||
ema = sum / period;
|
||||
results[i] = ema;
|
||||
}
|
||||
} else {
|
||||
ema = (candles[i][source] - ema) * multiplier + ema;
|
||||
results[i] = ema;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static rma(candles, period, source = 'close') {
|
||||
const multiplier = 1 / period;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let rma = 0;
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
if (i < period) {
|
||||
sum += candles[i][source];
|
||||
if (i === period - 1) {
|
||||
rma = sum / period;
|
||||
results[i] = rma;
|
||||
}
|
||||
} else {
|
||||
rma = (candles[i][source] - rma) * multiplier + rma;
|
||||
results[i] = rma;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static wma(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
const weightSum = (period * (period + 1)) / 2;
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += candles[i - j][source] * (period - j);
|
||||
}
|
||||
results[i] = sum / weightSum;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static vwma(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sumPV = 0;
|
||||
let sumV = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sumPV += candles[i - j][source] * candles[i - j].volume;
|
||||
sumV += candles[i - j].volume;
|
||||
}
|
||||
results[i] = sumV !== 0 ? sumPV / sumV : null;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
58
src/api/dashboard/static/js/indicators/macd.js
Normal file
58
src/api/dashboard/static/js/indicators/macd.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class MACDIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const fast = this.params.fast || 12;
|
||||
const slow = this.params.slow || 26;
|
||||
const signal = this.params.signal || 9;
|
||||
|
||||
const fastEma = MA.ema(candles, fast, 'close');
|
||||
const slowEma = MA.ema(candles, slow, 'close');
|
||||
|
||||
const macdLine = fastEma.map((f, i) => (f !== null && slowEma[i] !== null) ? f - slowEma[i] : null);
|
||||
|
||||
const signalLine = new Array(candles.length).fill(null);
|
||||
const multiplier = 2 / (signal + 1);
|
||||
let ema = 0;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < macdLine.length; i++) {
|
||||
if (macdLine[i] === null) continue;
|
||||
count++;
|
||||
if (count < signal) {
|
||||
sum += macdLine[i];
|
||||
} else if (count === signal) {
|
||||
sum += macdLine[i];
|
||||
ema = sum / signal;
|
||||
signalLine[i] = ema;
|
||||
} else {
|
||||
ema = (macdLine[i] - ema) * multiplier + ema;
|
||||
signalLine[i] = ema;
|
||||
}
|
||||
}
|
||||
|
||||
return macdLine.map((m, i) => ({
|
||||
macd: m,
|
||||
signal: signalLine[i],
|
||||
histogram: (m !== null && signalLine[i] !== null) ? m - signalLine[i] : null
|
||||
}));
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'MACD',
|
||||
inputs: [
|
||||
{ name: 'fast', label: 'Fast Period', type: 'number', default: 12 },
|
||||
{ name: 'slow', label: 'Slow Period', type: 'number', default: 26 },
|
||||
{ name: 'signal', label: 'Signal Period', type: 'number', default: 9 }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'macd', color: '#2196f3', title: 'MACD' },
|
||||
{ id: 'signal', color: '#ff5722', title: 'Signal' },
|
||||
{ id: 'histogram', color: '#607d8b', title: 'Histogram' }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
37
src/api/dashboard/static/js/indicators/rsi.js
Normal file
37
src/api/dashboard/static/js/indicators/rsi.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class RSIIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 14;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let gains = 0, losses = 0;
|
||||
for (let i = 1; i < candles.length; i++) {
|
||||
const diff = candles[i].close - candles[i-1].close;
|
||||
const gain = diff > 0 ? diff : 0;
|
||||
const loss = diff < 0 ? -diff : 0;
|
||||
if (i <= period) {
|
||||
gains += gain;
|
||||
losses += loss;
|
||||
if (i === period) {
|
||||
let avgGain = gains / period;
|
||||
let avgLoss = losses / period;
|
||||
results[i] = 100 - (100 / (1 + (avgGain / (avgLoss || 0.00001))));
|
||||
}
|
||||
} else {
|
||||
const lastAvgGain = (results[i-1] ? (results[i-1] > 0 ? (period-1) * (results[i-1] * (gains+losses)/100) : 0) : 0);
|
||||
gains = (gains * (period - 1) + gain) / period;
|
||||
losses = (losses * (period - 1) + loss) / period;
|
||||
results[i] = 100 - (100 / (1 + (gains / (losses || 0.00001))));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'RSI',
|
||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
||||
plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }]
|
||||
};
|
||||
}
|
||||
}
|
||||
17
src/api/dashboard/static/js/indicators/sma.js
Normal file
17
src/api/dashboard/static/js/indicators/sma.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class SMAIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 44;
|
||||
return MA.sma(candles, period, 'close');
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'SMA',
|
||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
|
||||
plots: [{ id: 'value', color: '#2962ff', title: 'SMA' }]
|
||||
};
|
||||
}
|
||||
}
|
||||
44
src/api/dashboard/static/js/indicators/stoch.js
Normal file
44
src/api/dashboard/static/js/indicators/stoch.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class StochasticIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const kPeriod = this.params.kPeriod || 14;
|
||||
const dPeriod = this.params.dPeriod || 3;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
const kValues = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = kPeriod - 1; i < candles.length; i++) {
|
||||
let lowest = Infinity;
|
||||
let highest = -Infinity;
|
||||
for (let j = 0; j < kPeriod; j++) {
|
||||
lowest = Math.min(lowest, candles[i-j].low);
|
||||
highest = Math.max(highest, candles[i-j].high);
|
||||
}
|
||||
const diff = highest - lowest;
|
||||
kValues[i] = diff === 0 ? 50 : ((candles[i].close - lowest) / diff) * 100;
|
||||
}
|
||||
|
||||
for (let i = kPeriod + dPeriod - 2; i < candles.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < dPeriod; j++) sum += kValues[i-j];
|
||||
results[i] = { k: kValues[i], d: sum / dPeriod };
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Stochastic',
|
||||
inputs: [
|
||||
{ name: 'kPeriod', label: 'K Period', type: 'number', default: 14 },
|
||||
{ name: 'dPeriod', label: 'D Period', type: 'number', default: 3 }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'k', color: '#3f51b5', title: '%K' },
|
||||
{ id: 'd', color: '#ff9800', title: '%D' }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
16
src/api/dashboard/static/js/strategies/config.js
Normal file
16
src/api/dashboard/static/js/strategies/config.js
Normal file
@ -0,0 +1,16 @@
|
||||
export const StrategyParams = {
|
||||
ma_trend: [
|
||||
{ name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 }
|
||||
]
|
||||
};
|
||||
|
||||
export const AVAILABLE_INDICATORS = [
|
||||
{ type: 'hts', name: 'HTS Trend System', description: 'Fast/Slow MAs of High/Low prices' },
|
||||
{ type: 'sma', name: 'SMA', description: 'Simple Moving Average' },
|
||||
{ type: 'ema', name: 'EMA', description: 'Exponential Moving Average' },
|
||||
{ type: 'rsi', name: 'RSI', description: 'Relative Strength Index' },
|
||||
{ type: 'bb', name: 'Bollinger Bands', description: 'Volatility bands' },
|
||||
{ type: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence' },
|
||||
{ type: 'stoch', name: 'Stochastic', description: 'Stochastic Oscillator' },
|
||||
{ type: 'atr', name: 'ATR', description: 'Average True Range' }
|
||||
];
|
||||
167
src/api/dashboard/static/js/strategies/engine.js
Normal file
167
src/api/dashboard/static/js/strategies/engine.js
Normal file
@ -0,0 +1,167 @@
|
||||
import { IndicatorRegistry } from '../indicators/index.js';
|
||||
import { RiskManager } from './risk-manager.js';
|
||||
|
||||
export class ClientStrategyEngine {
|
||||
constructor() {
|
||||
this.indicatorTypes = IndicatorRegistry;
|
||||
}
|
||||
|
||||
run(candlesMap, strategyConfig, riskConfig, simulationStart) {
|
||||
const primaryTF = strategyConfig.timeframes?.primary || '1d';
|
||||
const candles = candlesMap[primaryTF];
|
||||
if (!candles) return { error: `No candles for primary timeframe ${primaryTF}` };
|
||||
|
||||
const indicatorResults = {};
|
||||
console.log('Calculating indicators for timeframes:', Object.keys(candlesMap));
|
||||
for (const tf in candlesMap) {
|
||||
indicatorResults[tf] = {};
|
||||
const tfCandles = candlesMap[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) {
|
||||
const IndicatorClass = this.indicatorTypes[ind.type];
|
||||
if (IndicatorClass) {
|
||||
const instance = new IndicatorClass(ind);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const risk = new RiskManager(riskConfig);
|
||||
const trades = [];
|
||||
let position = null;
|
||||
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);
|
||||
|
||||
const pointers = {};
|
||||
for (const tf in candlesMap) pointers[tf] = 0;
|
||||
|
||||
let processedCandles = 0;
|
||||
|
||||
for (let i = 1; i < candles.length; i++) {
|
||||
const time = candles[i].time;
|
||||
const price = candles[i].close;
|
||||
|
||||
if (time < startTimeSec) {
|
||||
for (const tf in candlesMap) {
|
||||
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
||||
candlesMap[tf][pointers[tf] + 1].time <= time) {
|
||||
pointers[tf]++;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
processedCandles++;
|
||||
|
||||
for (const tf in candlesMap) {
|
||||
while (pointers[tf] < candlesMap[tf].length - 1 &&
|
||||
candlesMap[tf][pointers[tf] + 1].time <= time) {
|
||||
pointers[tf]++;
|
||||
}
|
||||
}
|
||||
|
||||
const signal = this.evaluate(i, pointers, candles, candlesMap, indicatorResults, strategyConfig, position);
|
||||
|
||||
if (signal === 'BUY' && !position) {
|
||||
const size = risk.calculateSize(price);
|
||||
position = { type: 'long', entryPrice: price, entryTime: candles[i].time, size };
|
||||
} else if (signal === 'SELL' && position) {
|
||||
const pnl = (price - position.entryPrice) * position.size;
|
||||
trades.push({ ...position, exitPrice: price, exitTime: candles[i].time, pnl, pnlPct: (pnl / (position.entryPrice * position.size)) * 100 });
|
||||
risk.balance += pnl;
|
||||
position = null;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Simulation complete: ${processedCandles} candles processed after start date, ${trades.length} trades`);
|
||||
|
||||
return {
|
||||
total_trades: trades.length,
|
||||
win_rate: (trades.filter(t => t.pnl > 0).length / (trades.length || 1)) * 100,
|
||||
total_pnl: risk.balance - 1000,
|
||||
trades
|
||||
};
|
||||
}
|
||||
|
||||
evaluate(index, pointers, candles, candlesMap, indicatorResults, config, position) {
|
||||
const primaryTF = config.timeframes?.primary || '1d';
|
||||
|
||||
const getVal = (indName, tf) => {
|
||||
const tfValues = indicatorResults[tf]?.[indName];
|
||||
if (!tfValues) return null;
|
||||
return tfValues[pointers[tf]];
|
||||
};
|
||||
|
||||
const getPrice = (tf) => {
|
||||
const tfCandles = candlesMap[tf];
|
||||
if (!tfCandles) return null;
|
||||
return tfCandles[pointers[tf]].close;
|
||||
};
|
||||
|
||||
if (config.id === 'ma_trend') {
|
||||
const period = config.params?.period || 44;
|
||||
|
||||
if (index === 1) {
|
||||
console.log('First candle time:', candles[index].time, 'Date:', new Date(candles[index].time * 1000));
|
||||
console.log(`MA${period} value:`, getVal(`ma${period}`, primaryTF));
|
||||
}
|
||||
const maValue = getVal(`ma${period}`, primaryTF);
|
||||
const price = candles[index].close;
|
||||
|
||||
const secondaryTF = config.timeframes?.secondary?.[0];
|
||||
let secondaryBullish = true;
|
||||
let secondaryBearish = true;
|
||||
if (secondaryTF) {
|
||||
const secondaryPrice = getPrice(secondaryTF);
|
||||
const secondaryMA = getVal(`ma${period}_${secondaryTF}`, secondaryTF);
|
||||
if (secondaryPrice !== null && secondaryMA !== null) {
|
||||
secondaryBullish = secondaryPrice > secondaryMA;
|
||||
secondaryBearish = secondaryPrice < secondaryMA;
|
||||
}
|
||||
if (index === 1) {
|
||||
console.log(`Trend check: ${secondaryTF} price=${secondaryPrice}, MA=${secondaryMA}, bullish=${secondaryBullish}, bearish=${secondaryBearish}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (maValue) {
|
||||
if (price > maValue && secondaryBullish) return 'BUY';
|
||||
if (price < maValue && secondaryBearish) return 'SELL';
|
||||
}
|
||||
}
|
||||
|
||||
const evaluateConditions = (conds) => {
|
||||
if (!conds || !conds.conditions) return false;
|
||||
const results = conds.conditions.map(c => {
|
||||
const targetTF = c.timeframe || primaryTF;
|
||||
const leftVal = c.indicator === 'price' ? getPrice(targetTF) : getVal(c.indicator, targetTF);
|
||||
const rightVal = typeof c.value === 'number' ? c.value : (c.value === 'price' ? getPrice(targetTF) : getVal(c.value, targetTF));
|
||||
|
||||
if (leftVal === null || rightVal === null) return false;
|
||||
|
||||
switch(c.operator) {
|
||||
case '>': return leftVal > rightVal;
|
||||
case '<': return leftVal < rightVal;
|
||||
case '>=': return leftVal >= rightVal;
|
||||
case '<=': return leftVal <= rightVal;
|
||||
case '==': return leftVal == rightVal;
|
||||
default: return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (conds.logic === 'OR') return results.some(r => r);
|
||||
return results.every(r => r);
|
||||
};
|
||||
|
||||
if (evaluateConditions(config.entryLong)) return 'BUY';
|
||||
if (evaluateConditions(config.exitLong)) return 'SELL';
|
||||
|
||||
return 'HOLD';
|
||||
}
|
||||
}
|
||||
3
src/api/dashboard/static/js/strategies/index.js
Normal file
3
src/api/dashboard/static/js/strategies/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
export { StrategyParams, AVAILABLE_INDICATORS } from './config.js';
|
||||
export { RiskManager } from './risk-manager.js';
|
||||
export { ClientStrategyEngine } from './engine.js';
|
||||
17
src/api/dashboard/static/js/strategies/risk-manager.js
Normal file
17
src/api/dashboard/static/js/strategies/risk-manager.js
Normal file
@ -0,0 +1,17 @@
|
||||
export class RiskManager {
|
||||
constructor(config, initialBalance = 1000) {
|
||||
this.config = config || {
|
||||
positionSizing: { method: 'percent', value: 0.1 },
|
||||
stopLoss: { enabled: true, method: 'percent', value: 0.02 },
|
||||
takeProfit: { enabled: true, method: 'percent', value: 0.04 }
|
||||
};
|
||||
this.balance = initialBalance;
|
||||
this.equity = initialBalance;
|
||||
}
|
||||
calculateSize(price) {
|
||||
if (this.config.positionSizing.method === 'percent') {
|
||||
return (this.balance * this.config.positionSizing.value) / price;
|
||||
}
|
||||
return this.config.positionSizing.value / price;
|
||||
}
|
||||
}
|
||||
548
src/api/dashboard/static/js/ui/chart.js
Normal file
548
src/api/dashboard/static/js/ui/chart.js
Normal file
@ -0,0 +1,548 @@
|
||||
import { INTERVALS, COLORS } from '../core/index.js';
|
||||
|
||||
export class TradingDashboard {
|
||||
constructor() {
|
||||
this.chart = null;
|
||||
this.candleSeries = null;
|
||||
this.currentInterval = '1d';
|
||||
this.intervals = INTERVALS;
|
||||
this.allData = new Map();
|
||||
this.isLoading = false;
|
||||
this.hasInitialLoad = false;
|
||||
this.taData = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createTimeframeButtons();
|
||||
this.initChart();
|
||||
this.initEventListeners();
|
||||
this.loadInitialData();
|
||||
|
||||
setInterval(() => {
|
||||
this.loadNewData();
|
||||
if (new Date().getSeconds() < 15) this.loadTA();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
isAtRightEdge() {
|
||||
const timeScale = this.chart.timeScale();
|
||||
const visibleRange = timeScale.getVisibleLogicalRange();
|
||||
if (!visibleRange) return true;
|
||||
|
||||
const data = this.candleSeries.data();
|
||||
if (!data || data.length === 0) return true;
|
||||
|
||||
return visibleRange.to >= data.length - 5;
|
||||
}
|
||||
|
||||
createTimeframeButtons() {
|
||||
const container = document.getElementById('timeframeContainer');
|
||||
container.innerHTML = '';
|
||||
this.intervals.forEach(interval => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'timeframe-btn';
|
||||
btn.dataset.interval = interval;
|
||||
btn.textContent = interval;
|
||||
if (interval === this.currentInterval) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
btn.addEventListener('click', () => this.switchTimeframe(interval));
|
||||
container.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
initChart() {
|
||||
const chartContainer = document.getElementById('chart');
|
||||
|
||||
this.chart = LightweightCharts.createChart(chartContainer, {
|
||||
layout: {
|
||||
background: { color: COLORS.tvBg },
|
||||
textColor: COLORS.tvText,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: COLORS.tvBorder },
|
||||
horzLines: { color: COLORS.tvBorder },
|
||||
},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderColor: COLORS.tvBorder,
|
||||
autoScale: true,
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: COLORS.tvBorder,
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
rightOffset: 12,
|
||||
barSpacing: 10,
|
||||
},
|
||||
handleScroll: {
|
||||
vertTouchDrag: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.candleSeries = this.chart.addCandlestickSeries({
|
||||
upColor: '#ff9800',
|
||||
downColor: '#ff9800',
|
||||
borderUpColor: '#ff9800',
|
||||
borderDownColor: '#ff9800',
|
||||
wickUpColor: '#ff9800',
|
||||
wickDownColor: '#ff9800',
|
||||
});
|
||||
|
||||
this.initPriceScaleControls();
|
||||
|
||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.chart.applyOptions({
|
||||
width: chartContainer.clientWidth,
|
||||
height: chartContainer.clientHeight,
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.loadNewData();
|
||||
this.loadTA();
|
||||
}
|
||||
});
|
||||
window.addEventListener('focus', () => {
|
||||
this.loadNewData();
|
||||
this.loadTA();
|
||||
});
|
||||
}
|
||||
|
||||
initPriceScaleControls() {
|
||||
const btnAutoScale = document.getElementById('btnAutoScale');
|
||||
const btnLogScale = document.getElementById('btnLogScale');
|
||||
|
||||
if (!btnAutoScale || !btnLogScale) return;
|
||||
|
||||
this.priceScaleState = {
|
||||
autoScale: true,
|
||||
logScale: false
|
||||
};
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
btnLogScale.addEventListener('click', () => {
|
||||
this.priceScaleState.logScale = !this.priceScaleState.logScale;
|
||||
btnLogScale.classList.toggle('active', this.priceScaleState.logScale);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
this.candleSeries.priceScale().applyOptions({
|
||||
mode: this.priceScaleState.logScale ? LightweightCharts.PriceScaleMode.Logarithmic : LightweightCharts.PriceScaleMode.Normal
|
||||
});
|
||||
|
||||
this.chart.applyOptions({});
|
||||
|
||||
setTimeout(() => {
|
||||
if (currentTimeRange) {
|
||||
try {
|
||||
this.chart.timeScale().setVisibleLogicalRange(currentTimeRange);
|
||||
} catch (e) {
|
||||
console.log('Could not restore time range');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'a' || e.key === 'A') {
|
||||
if (e.target.tagName !== 'INPUT') {
|
||||
btnAutoScale.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initEventListeners() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
|
||||
|
||||
const shortcuts = {
|
||||
'1': '1m', '2': '3m', '3': '5m', '4': '15m', '5': '30m', '7': '37m',
|
||||
'6': '1h', '8': '4h', '9': '8h', '0': '12h',
|
||||
'd': '1d', 'D': '1d', 'w': '1w', 'W': '1w', 'm': '1M', 'M': '1M'
|
||||
};
|
||||
|
||||
if (shortcuts[e.key]) {
|
||||
this.switchTimeframe(shortcuts[e.key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadInitialData() {
|
||||
await this.loadData(1000, true);
|
||||
this.hasInitialLoad = true;
|
||||
}
|
||||
|
||||
async loadData(limit = 1000, fitToContent = false) {
|
||||
if (this.isLoading) return;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
|
||||
|
||||
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`);
|
||||
const data = await response.json();
|
||||
|
||||
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),
|
||||
high: parseFloat(c.high),
|
||||
low: parseFloat(c.low),
|
||||
close: parseFloat(c.close)
|
||||
}));
|
||||
|
||||
const existingData = this.allData.get(this.currentInterval) || [];
|
||||
const mergedData = this.mergeData(existingData, chartData);
|
||||
this.allData.set(this.currentInterval, mergedData);
|
||||
|
||||
this.candleSeries.setData(mergedData);
|
||||
|
||||
if (fitToContent) {
|
||||
this.chart.timeScale().fitContent();
|
||||
} else if (visibleRange) {
|
||||
this.chart.timeScale().setVisibleLogicalRange(visibleRange);
|
||||
}
|
||||
|
||||
const latest = mergedData[mergedData.length - 1];
|
||||
this.updateStats(latest);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadNewData() {
|
||||
if (!this.hasInitialLoad || this.isLoading) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.candles && data.candles.length > 0) {
|
||||
const atEdge = this.isAtRightEdge();
|
||||
|
||||
const currentSeriesData = this.candleSeries.data();
|
||||
const lastTimestamp = currentSeriesData.length > 0
|
||||
? currentSeriesData[currentSeriesData.length - 1].time
|
||||
: 0;
|
||||
|
||||
const chartData = data.candles.reverse().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)
|
||||
}));
|
||||
|
||||
chartData.forEach(candle => {
|
||||
if (candle.time >= lastTimestamp) {
|
||||
this.candleSeries.update(candle);
|
||||
}
|
||||
});
|
||||
|
||||
const existingData = this.allData.get(this.currentInterval) || [];
|
||||
this.allData.set(this.currentInterval, this.mergeData(existingData, chartData));
|
||||
|
||||
if (atEdge) {
|
||||
this.chart.timeScale().scrollToRealTime();
|
||||
}
|
||||
|
||||
const latest = chartData[chartData.length - 1];
|
||||
this.updateStats(latest);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading new data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
mergeData(existing, newData) {
|
||||
const dataMap = new Map();
|
||||
existing.forEach(c => dataMap.set(c.time, c));
|
||||
newData.forEach(c => dataMap.set(c.time, c));
|
||||
return Array.from(dataMap.values()).sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
onVisibleRangeChange() {
|
||||
if (!this.hasInitialLoad || this.isLoading) {
|
||||
console.log('Skipping range change:', { hasInitialLoad: this.hasInitialLoad, isLoading: this.isLoading });
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
|
||||
if (!visibleRange) {
|
||||
console.log('No visible range');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.candleSeries.data();
|
||||
if (!data || data.length === 0) {
|
||||
console.log('No data available');
|
||||
return;
|
||||
}
|
||||
|
||||
const barsFromLeft = visibleRange.from;
|
||||
const totalBars = data.length;
|
||||
|
||||
console.log('Visible range:', { from: visibleRange.from, to: visibleRange.to, barsFromLeft, totalBars });
|
||||
|
||||
if (barsFromLeft < 50) {
|
||||
console.log('Near left edge (within 50 bars), loading historical data...');
|
||||
const oldestCandle = data[0];
|
||||
if (oldestCandle) {
|
||||
this.loadHistoricalData(oldestCandle.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadHistoricalData(beforeTime) {
|
||||
if (this.isLoading) {
|
||||
console.log('Already loading, skipping...');
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
console.log(`Loading historical data for ${this.currentInterval} before ${beforeTime}`);
|
||||
|
||||
const currentData = this.candleSeries.data();
|
||||
const visibleRange = this.chart.timeScale().getVisibleLogicalRange();
|
||||
let leftmostTime = null;
|
||||
|
||||
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 {
|
||||
const endTime = new Date((beforeTime - 1) * 1000);
|
||||
|
||||
console.log(`Loading historical data before ${new Date((beforeTime - 1) * 1000).toISOString()}`);
|
||||
|
||||
const response = await fetch(
|
||||
`/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();
|
||||
|
||||
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),
|
||||
high: parseFloat(c.high),
|
||||
low: parseFloat(c.low),
|
||||
close: parseFloat(c.close)
|
||||
}));
|
||||
|
||||
const existingData = this.allData.get(this.currentInterval) || [];
|
||||
const mergedData = this.mergeData(existingData, chartData);
|
||||
this.allData.set(this.currentInterval, mergedData);
|
||||
|
||||
this.candleSeries.setData(mergedData);
|
||||
|
||||
if (leftmostTime) {
|
||||
const idx = mergedData.findIndex(c => c.time === leftmostTime);
|
||||
if (idx >= 0) {
|
||||
this.chart.timeScale().setVisibleLogicalRange({ from: idx, to: idx + 50 });
|
||||
}
|
||||
}
|
||||
|
||||
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`);
|
||||
} else {
|
||||
console.log('No more historical data available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading historical data:', error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadTA() {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/ta?symbol=BTC&interval=${this.currentInterval}`);
|
||||
this.taData = await response.json();
|
||||
this.renderTA();
|
||||
} catch (error) {
|
||||
console.error('Error loading TA:', error);
|
||||
document.getElementById('taContent').innerHTML = '<div class="ta-error">Failed to load technical analysis</div>';
|
||||
}
|
||||
}
|
||||
|
||||
renderTA() {
|
||||
if (!this.taData || this.taData.error) {
|
||||
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.taData;
|
||||
const trendClass = data.trend.direction.toLowerCase();
|
||||
const signalClass = data.trend.signal.toLowerCase();
|
||||
|
||||
const ma44Change = data.moving_averages.price_vs_ma44;
|
||||
const ma125Change = data.moving_averages.price_vs_ma125;
|
||||
|
||||
document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase();
|
||||
document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString();
|
||||
|
||||
document.getElementById('taContent').innerHTML = `
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Trend Analysis</div>
|
||||
<div class="ta-trend ${trendClass}">
|
||||
${data.trend.direction} ${trendClass === 'bullish' ? '↑' : trendClass === 'bearish' ? '↓' : '→'}
|
||||
</div>
|
||||
<div class="ta-strength">${data.trend.strength}</div>
|
||||
<span class="ta-signal ${signalClass}">${data.trend.signal}</span>
|
||||
</div>
|
||||
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Moving Averages</div>
|
||||
<div class="ta-ma-row">
|
||||
<span class="ta-ma-label">MA 44</span>
|
||||
<span class="ta-ma-value">
|
||||
${data.moving_averages.ma_44 ? data.moving_averages.ma_44.toFixed(2) : 'N/A'}
|
||||
${ma44Change !== null ? `<span class="ta-ma-change ${ma44Change >= 0 ? 'positive' : 'negative'}">${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ta-ma-row">
|
||||
<span class="ta-ma-label">MA 125</span>
|
||||
<span class="ta-ma-value">
|
||||
${data.moving_averages.ma_125 ? data.moving_averages.ma_125.toFixed(2) : 'N/A'}
|
||||
${ma125Change !== null ? `<span class="ta-ma-change ${ma125Change >= 0 ? 'positive' : 'negative'}">${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ta-section">
|
||||
<div class="ta-section-title">Indicators</div>
|
||||
<div id="indicatorList" style="display: flex; flex-direction: column; gap: 6px; margin-top: 8px;"></div>
|
||||
<button class="ta-btn" onclick="addNewIndicator()" style="width: 100%; margin-top: 12px; font-size: 11px;">
|
||||
+ Add Indicator
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ta-section" id="indicatorConfigPanel">
|
||||
<div class="ta-section-title">Configuration</div>
|
||||
<div id="configForm" style="margin-top: 8px;"></div>
|
||||
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||||
<button class="ta-btn" onclick="applyIndicatorConfig()" style="flex: 1; font-size: 11px; background: var(--tv-blue); color: white; border: none;">Apply</button>
|
||||
<button class="ta-btn" onclick="removeIndicator()" style="flex: 1; font-size: 11px; border-color: var(--tv-red); color: var(--tv-red);">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updateStats(candle) {
|
||||
const price = candle.close;
|
||||
const change = ((price - candle.open) / candle.open * 100);
|
||||
|
||||
document.getElementById('currentPrice').textContent = price.toFixed(2);
|
||||
document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
|
||||
document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
|
||||
document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
|
||||
document.getElementById('dailyHigh').textContent = candle.high.toFixed(2);
|
||||
document.getElementById('dailyLow').textContent = candle.low.toFixed(2);
|
||||
}
|
||||
|
||||
switchTimeframe(interval) {
|
||||
if (!this.intervals.includes(interval) || interval === this.currentInterval) return;
|
||||
|
||||
this.currentInterval = interval;
|
||||
this.hasInitialLoad = false;
|
||||
|
||||
document.querySelectorAll('.timeframe-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.interval === interval);
|
||||
});
|
||||
|
||||
this.allData.delete(interval);
|
||||
this.loadInitialData();
|
||||
this.loadTA();
|
||||
|
||||
window.clearSimulationResults?.();
|
||||
window.updateTimeframeDisplay?.();
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshTA() {
|
||||
if (window.dashboard) {
|
||||
window.dashboard.loadTA();
|
||||
}
|
||||
}
|
||||
|
||||
export function openAIAnalysis() {
|
||||
const symbol = 'BTC';
|
||||
const interval = window.dashboard?.currentInterval || '1d';
|
||||
const prompt = `Analyze Bitcoin (${symbol}) ${interval} chart. Current trend, support/resistance levels, and trading recommendation. Technical indicators: MA44, MA125.`;
|
||||
|
||||
const geminiUrl = `https://gemini.google.com/app?prompt=${encodeURIComponent(prompt)}`;
|
||||
window.open(geminiUrl, '_blank');
|
||||
}
|
||||
140
src/api/dashboard/static/js/ui/export.js
Normal file
140
src/api/dashboard/static/js/ui/export.js
Normal file
@ -0,0 +1,140 @@
|
||||
import { downloadFile } from '../utils/index.js';
|
||||
|
||||
export function showExportDialog() {
|
||||
if (!window.lastSimulationResults) {
|
||||
alert('Please run a simulation first');
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'dialog-overlay';
|
||||
overlay.onclick = () => closeExportDialog();
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'export-dialog';
|
||||
dialog.id = 'exportDialog';
|
||||
dialog.innerHTML = `
|
||||
<div class="export-dialog-title">📥 Export Simulation Report</div>
|
||||
<div class="export-options">
|
||||
<label class="export-option">
|
||||
<input type="radio" name="exportFormat" value="csv" checked>
|
||||
<span>CSV (Trades list)</span>
|
||||
</label>
|
||||
<label class="export-option">
|
||||
<input type="radio" name="exportFormat" value="json">
|
||||
<span>JSON (Full data)</span>
|
||||
</label>
|
||||
<label class="export-option">
|
||||
<input type="radio" name="exportFormat" value="both">
|
||||
<span>Both CSV + JSON</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="action-btn secondary" onclick="closeExportDialog()" style="flex: 1;">Cancel</button>
|
||||
<button class="action-btn primary" onclick="performExport()" style="flex: 1;">Export</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
}
|
||||
|
||||
export function closeExportDialog() {
|
||||
const overlay = document.querySelector('.dialog-overlay');
|
||||
const dialog = document.getElementById('exportDialog');
|
||||
if (overlay) overlay.remove();
|
||||
if (dialog) dialog.remove();
|
||||
}
|
||||
|
||||
export function performExport() {
|
||||
const format = document.querySelector('input[name="exportFormat"]:checked').value;
|
||||
const sim = window.lastSimulationResults;
|
||||
const config = sim.config || {};
|
||||
const dateStr = new Date().toISOString().slice(0, 10);
|
||||
const baseFilename = generateSimulationName(config).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
|
||||
if (format === 'csv' || format === 'both') {
|
||||
exportToCSV(sim, `${baseFilename}.csv`);
|
||||
}
|
||||
|
||||
if (format === 'json' || format === 'both') {
|
||||
exportToJSON(sim, `${baseFilename}.json`);
|
||||
}
|
||||
|
||||
closeExportDialog();
|
||||
}
|
||||
|
||||
function generateSimulationName(config) {
|
||||
if (!config) return 'Unnamed Simulation';
|
||||
|
||||
const start = new Date(config.startDate);
|
||||
const now = new Date();
|
||||
const duration = now - start;
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
let dateStr;
|
||||
if (duration < oneDay) {
|
||||
dateStr = start.toISOString().slice(0, 16).replace('T', ' ');
|
||||
} else {
|
||||
dateStr = start.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
return `${config.strategyName}_${config.timeframe}_${dateStr}`;
|
||||
}
|
||||
|
||||
function exportToCSV(simulation, filename) {
|
||||
const results = simulation.results || simulation;
|
||||
const config = simulation.config || {};
|
||||
|
||||
let csv = 'Trade #,Entry Time,Exit Time,Entry Price,Exit Price,Size,P&L ($),P&L (%),Type\n';
|
||||
|
||||
(results.trades || []).forEach((trade, i) => {
|
||||
csv += `${i + 1},${trade.entryTime},${trade.exitTime},${trade.entryPrice},${trade.exitPrice},${trade.size},${trade.pnl},${trade.pnlPct},${trade.type}\n`;
|
||||
});
|
||||
|
||||
csv += '\n';
|
||||
csv += 'Summary\n';
|
||||
csv += `Strategy,${config.strategyName || 'Unknown'}\n`;
|
||||
csv += `Timeframe,${config.timeframe || 'Unknown'}\n`;
|
||||
csv += `Start Date,${config.startDate || 'Unknown'}\n`;
|
||||
csv += `Total Trades,${results.total_trades || 0}\n`;
|
||||
csv += `Win Rate (%),${(results.win_rate || 0).toFixed(2)}\n`;
|
||||
csv += `Total P&L ($),${(results.total_pnl || 0).toFixed(2)}\n`;
|
||||
csv += `Risk % per Trade,${config.riskPercent || 2}\n`;
|
||||
csv += `Stop Loss %,${config.stopLossPercent || 2}\n`;
|
||||
|
||||
downloadFile(csv, filename, 'text/csv');
|
||||
}
|
||||
|
||||
function exportToJSON(simulation, filename) {
|
||||
const exportData = {
|
||||
metadata: {
|
||||
exported_at: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
},
|
||||
configuration: simulation.config || {},
|
||||
results: {
|
||||
summary: {
|
||||
total_trades: simulation.total_trades || simulation.results?.total_trades || 0,
|
||||
win_rate: simulation.win_rate || simulation.results?.win_rate || 0,
|
||||
total_pnl: simulation.total_pnl || simulation.results?.total_pnl || 0
|
||||
},
|
||||
trades: simulation.trades || simulation.results?.trades || [],
|
||||
equity_curve: simulation.equity_curve || []
|
||||
}
|
||||
};
|
||||
|
||||
downloadFile(JSON.stringify(exportData, null, 2), filename, 'application/json');
|
||||
}
|
||||
|
||||
export function exportSavedSimulation(id) {
|
||||
const sim = window.SimulationStorage?.get(id);
|
||||
if (!sim) {
|
||||
alert('Simulation not found');
|
||||
return;
|
||||
}
|
||||
|
||||
window.lastSimulationResults = sim;
|
||||
showExportDialog();
|
||||
}
|
||||
|
||||
window.generateSimulationName = generateSimulationName;
|
||||
37
src/api/dashboard/static/js/ui/index.js
Normal file
37
src/api/dashboard/static/js/ui/index.js
Normal file
@ -0,0 +1,37 @@
|
||||
export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js';
|
||||
export { toggleSidebar, restoreSidebarState } from './sidebar.js';
|
||||
export { SimulationStorage } from './storage.js';
|
||||
export { showExportDialog, closeExportDialog, performExport, exportSavedSimulation } from './export.js';
|
||||
export {
|
||||
runSimulation,
|
||||
displayEnhancedResults,
|
||||
showSimulationMarkers,
|
||||
clearSimulationMarkers,
|
||||
clearSimulationResults,
|
||||
getLastResults,
|
||||
setLastResults
|
||||
} from './simulation.js';
|
||||
export {
|
||||
renderStrategies,
|
||||
selectStrategy,
|
||||
renderStrategyParams,
|
||||
loadStrategies,
|
||||
saveSimulation,
|
||||
renderSavedSimulations,
|
||||
loadSavedSimulation,
|
||||
deleteSavedSimulation,
|
||||
getCurrentStrategy,
|
||||
setCurrentStrategy
|
||||
} from './strategies-panel.js';
|
||||
export {
|
||||
renderIndicatorList,
|
||||
addNewIndicator,
|
||||
selectIndicator,
|
||||
renderIndicatorConfig,
|
||||
applyIndicatorConfig,
|
||||
removeIndicator,
|
||||
removeIndicatorByIndex,
|
||||
drawIndicatorsOnChart,
|
||||
getActiveIndicators,
|
||||
setActiveIndicators
|
||||
} from './indicators-panel.js';
|
||||
202
src/api/dashboard/static/js/ui/indicators-panel.js
Normal file
202
src/api/dashboard/static/js/ui/indicators-panel.js
Normal file
@ -0,0 +1,202 @@
|
||||
import { AVAILABLE_INDICATORS } from '../strategies/config.js';
|
||||
import { IndicatorRegistry as IR } from '../indicators/index.js';
|
||||
|
||||
let activeIndicators = [];
|
||||
let selectedIndicatorIndex = -1;
|
||||
|
||||
export function getActiveIndicators() {
|
||||
return activeIndicators;
|
||||
}
|
||||
|
||||
export function setActiveIndicators(indicators) {
|
||||
activeIndicators = indicators;
|
||||
}
|
||||
|
||||
export function renderIndicatorList() {
|
||||
const container = document.getElementById('indicatorList');
|
||||
if (!container) return;
|
||||
|
||||
if (activeIndicators.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">No indicators added</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = activeIndicators.map((ind, idx) => `
|
||||
<div class="indicator-item ${idx === selectedIndicatorIndex ? 'selected' : ''}"
|
||||
onclick="selectIndicator(${idx})"
|
||||
style="display: flex; align-items: center; justify-content: space-between; padding: 8px; background: var(--tv-bg); border-radius: 4px; cursor: pointer; border: 1px solid ${idx === selectedIndicatorIndex ? 'var(--tv-blue)' : 'var(--tv-border)'}">
|
||||
<div>
|
||||
<div style="font-size: 12px; font-weight: 600;">${ind.name}</div>
|
||||
<div style="font-size: 10px; color: var(--tv-text-secondary);">${ind.params.short || ind.params.period || ind.params.fast || 'N/A'}</div>
|
||||
</div>
|
||||
<button onclick="event.stopPropagation(); removeIndicatorByIndex(${idx})" style="background: none; border: none; color: var(--tv-red); cursor: pointer; font-size: 14px;">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const configPanel = document.getElementById('indicatorConfigPanel');
|
||||
if (selectedIndicatorIndex >= 0) {
|
||||
configPanel.style.display = 'block';
|
||||
renderIndicatorConfig(selectedIndicatorIndex);
|
||||
} else {
|
||||
configPanel.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export function addNewIndicator() {
|
||||
const type = prompt('Enter indicator type:\n' + AVAILABLE_INDICATORS.map(i => `${i.type}: ${i.name}`).join('\n'));
|
||||
if (!type) return;
|
||||
|
||||
const indicatorDef = AVAILABLE_INDICATORS.find(i => i.type === type.toLowerCase());
|
||||
if (!indicatorDef) {
|
||||
alert('Unknown indicator type');
|
||||
return;
|
||||
}
|
||||
|
||||
const IndicatorClass = IR?.[type.toLowerCase()];
|
||||
if (!IndicatorClass) {
|
||||
alert('Indicator class not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = new IndicatorClass({ type: type.toLowerCase(), params: {}, name: indicatorDef.name });
|
||||
const metadata = instance.getMetadata();
|
||||
|
||||
const params = {};
|
||||
metadata.inputs.forEach(input => {
|
||||
params[input.name] = input.default;
|
||||
});
|
||||
|
||||
activeIndicators.push({
|
||||
type: type.toLowerCase(),
|
||||
name: metadata.name,
|
||||
params: params,
|
||||
plots: metadata.plots,
|
||||
series: []
|
||||
});
|
||||
|
||||
selectedIndicatorIndex = activeIndicators.length - 1;
|
||||
renderIndicatorList();
|
||||
drawIndicatorsOnChart();
|
||||
}
|
||||
|
||||
export function selectIndicator(index) {
|
||||
selectedIndicatorIndex = index;
|
||||
renderIndicatorList();
|
||||
}
|
||||
|
||||
export function renderIndicatorConfig(index) {
|
||||
const container = document.getElementById('configForm');
|
||||
const indicator = activeIndicators[index];
|
||||
if (!indicator || !indicator.plots) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||
const meta = instance.getMetadata();
|
||||
|
||||
container.innerHTML = meta.inputs.map(input => `
|
||||
<div style="margin-bottom: 8px;">
|
||||
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
||||
${input.type === 'select' ?
|
||||
`<select id="config_${input.name}" class="sim-input" style="font-size: 12px; padding: 6px;">${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
|
||||
`<input type="number" id="config_${input.name}" class="sim-input" value="${indicator.params[input.name]}" ${input.min !== undefined ? `min="${input.min}"` : ''} ${input.max !== undefined ? `max="${input.max}"` : ''} style="font-size: 12px; padding: 6px;">`
|
||||
}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
export function applyIndicatorConfig() {
|
||||
if (selectedIndicatorIndex < 0) return;
|
||||
|
||||
const indicator = activeIndicators[selectedIndicatorIndex];
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
|
||||
const meta = instance.getMetadata();
|
||||
|
||||
meta.inputs.forEach(input => {
|
||||
const el = document.getElementById(`config_${input.name}`);
|
||||
if (el) {
|
||||
indicator.params[input.name] = input.type === 'select' ? el.value : parseFloat(el.value);
|
||||
}
|
||||
});
|
||||
|
||||
renderIndicatorList();
|
||||
drawIndicatorsOnChart();
|
||||
}
|
||||
|
||||
export function removeIndicator() {
|
||||
if (selectedIndicatorIndex < 0) return;
|
||||
activeIndicators.splice(selectedIndicatorIndex, 1);
|
||||
selectedIndicatorIndex = -1;
|
||||
renderIndicatorList();
|
||||
drawIndicatorsOnChart();
|
||||
}
|
||||
|
||||
export function removeIndicatorByIndex(index) {
|
||||
activeIndicators.splice(index, 1);
|
||||
if (selectedIndicatorIndex >= activeIndicators.length) {
|
||||
selectedIndicatorIndex = activeIndicators.length - 1;
|
||||
}
|
||||
renderIndicatorList();
|
||||
drawIndicatorsOnChart();
|
||||
}
|
||||
|
||||
export function drawIndicatorsOnChart() {
|
||||
if (!window.dashboard || !window.dashboard.chart) return;
|
||||
|
||||
activeIndicators.forEach(ind => {
|
||||
ind.series?.forEach(s => {
|
||||
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
|
||||
});
|
||||
});
|
||||
|
||||
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||
if (!candles || candles.length === 0) return;
|
||||
|
||||
activeIndicators.forEach((indicator, idx) => {
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
if (!IndicatorClass) return;
|
||||
|
||||
const instance = new IndicatorClass({
|
||||
type: indicator.type,
|
||||
params: indicator.params,
|
||||
name: indicator.name
|
||||
});
|
||||
|
||||
const results = instance.calculate(candles);
|
||||
const meta = instance.getMetadata();
|
||||
indicator.series = [];
|
||||
|
||||
meta.plots.forEach(plot => {
|
||||
if (results[0] && typeof results[0][plot.id] === 'undefined') return;
|
||||
|
||||
const lineSeries = window.dashboard.chart.addLineSeries({
|
||||
color: plot.color || '#2962ff',
|
||||
lineWidth: plot.width || 1,
|
||||
title: plot.title,
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: true
|
||||
});
|
||||
|
||||
const data = candles.map((c, i) => ({
|
||||
time: c.time,
|
||||
value: results[i]?.[plot.id] ?? null
|
||||
})).filter(d => d.value !== null && d.value !== undefined);
|
||||
|
||||
lineSeries.setData(data);
|
||||
indicator.series.push(lineSeries);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addNewIndicator = addNewIndicator;
|
||||
window.selectIndicator = selectIndicator;
|
||||
window.applyIndicatorConfig = applyIndicatorConfig;
|
||||
window.removeIndicator = removeIndicator;
|
||||
window.removeIndicatorByIndex = removeIndicatorByIndex;
|
||||
23
src/api/dashboard/static/js/ui/sidebar.js
Normal file
23
src/api/dashboard/static/js/ui/sidebar.js
Normal file
@ -0,0 +1,23 @@
|
||||
export function toggleSidebar() {
|
||||
const sidebar = document.getElementById('rightSidebar');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
|
||||
|
||||
// Resize chart after sidebar toggle
|
||||
setTimeout(() => {
|
||||
if (window.dashboard && window.dashboard.chart) {
|
||||
const container = document.getElementById('chart');
|
||||
window.dashboard.chart.applyOptions({
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight
|
||||
});
|
||||
}
|
||||
}, 350); // Wait for CSS transition
|
||||
}
|
||||
|
||||
export function restoreSidebarState() {
|
||||
const collapsed = localStorage.getItem('sidebar_collapsed') === 'true';
|
||||
if (collapsed) {
|
||||
document.getElementById('rightSidebar').classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
377
src/api/dashboard/static/js/ui/simulation.js
Normal file
377
src/api/dashboard/static/js/ui/simulation.js
Normal file
@ -0,0 +1,377 @@
|
||||
import { ClientStrategyEngine } from '../strategies/index.js';
|
||||
import { SimulationStorage } from './storage.js';
|
||||
import { downloadFile } from '../utils/index.js';
|
||||
import { showExportDialog, closeExportDialog, performExport } from './export.js';
|
||||
|
||||
let lastSimulationResults = null;
|
||||
|
||||
export function getLastResults() {
|
||||
return lastSimulationResults;
|
||||
}
|
||||
|
||||
export function setLastResults(results) {
|
||||
lastSimulationResults = results;
|
||||
window.lastSimulationResults = results;
|
||||
}
|
||||
|
||||
export async function runSimulation() {
|
||||
const strategyConfig = getStrategyConfig();
|
||||
if (!strategyConfig) {
|
||||
alert('Please select a strategy');
|
||||
return;
|
||||
}
|
||||
|
||||
const startDateInput = document.getElementById('simStartDate').value;
|
||||
if (!startDateInput) {
|
||||
alert('Please select a start date');
|
||||
return;
|
||||
}
|
||||
|
||||
const runBtn = document.getElementById('runSimBtn');
|
||||
runBtn.disabled = true;
|
||||
runBtn.textContent = '⏳ Running...';
|
||||
|
||||
try {
|
||||
const start = new Date(startDateInput);
|
||||
const fetchStart = new Date(start.getTime() - 200 * 24 * 60 * 60 * 1000);
|
||||
|
||||
if (!window.dashboard) {
|
||||
throw new Error('Dashboard not initialized');
|
||||
}
|
||||
const interval = window.dashboard.currentInterval;
|
||||
const secondaryTF = document.getElementById('simSecondaryTF').value;
|
||||
const riskPercent = parseFloat(document.getElementById('simRiskPercent').value);
|
||||
const stopLossPercent = parseFloat(document.getElementById('simStopLoss').value);
|
||||
|
||||
const timeframes = [interval];
|
||||
if (secondaryTF && secondaryTF !== '') {
|
||||
timeframes.push(secondaryTF);
|
||||
}
|
||||
|
||||
const query = new URLSearchParams({ symbol: 'BTC', start: fetchStart.toISOString() });
|
||||
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));
|
||||
|
||||
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 = {
|
||||
[interval]: data[interval].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)
|
||||
}))
|
||||
};
|
||||
|
||||
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)
|
||||
}));
|
||||
}
|
||||
|
||||
const engineConfig = {
|
||||
id: strategyConfig.id,
|
||||
params: strategyConfig.params,
|
||||
timeframes: { primary: interval, secondary: secondaryTF ? [secondaryTF] : [] },
|
||||
indicators: []
|
||||
};
|
||||
|
||||
console.log('Building strategy config:');
|
||||
console.log(' Primary TF:', interval);
|
||||
console.log(' Secondary TF:', secondaryTF);
|
||||
console.log(' Available candles:', Object.keys(candlesMap));
|
||||
|
||||
if (strategyConfig.id === 'ma_trend') {
|
||||
const period = strategyConfig.params?.period || 44;
|
||||
engineConfig.indicators.push({
|
||||
name: `ma${period}`,
|
||||
type: 'sma',
|
||||
params: { period: period },
|
||||
timeframe: interval
|
||||
});
|
||||
if (secondaryTF) {
|
||||
engineConfig.indicators.push({
|
||||
name: `ma${period}_${secondaryTF}`,
|
||||
type: 'sma',
|
||||
params: { period: period },
|
||||
timeframe: secondaryTF
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Indicators configured:', engineConfig.indicators.map(i => `${i.name} on ${i.timeframe}`));
|
||||
|
||||
const riskConfig = {
|
||||
positionSizing: { method: 'percent', value: riskPercent },
|
||||
stopLoss: { enabled: true, method: 'percent', value: stopLossPercent }
|
||||
};
|
||||
|
||||
const engine = new ClientStrategyEngine();
|
||||
const results = engine.run(candlesMap, engineConfig, riskConfig, start);
|
||||
|
||||
if (results.error) throw new Error(results.error);
|
||||
|
||||
setLastResults({
|
||||
...results,
|
||||
config: {
|
||||
strategyId: strategyConfig.id,
|
||||
strategyName: window.availableStrategies?.find(s => s.id === strategyConfig.id)?.name || strategyConfig.id,
|
||||
timeframe: interval,
|
||||
secondaryTimeframe: secondaryTF,
|
||||
startDate: startDateInput,
|
||||
riskPercent: riskPercent,
|
||||
stopLossPercent: stopLossPercent,
|
||||
params: strategyConfig.params
|
||||
},
|
||||
runAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
displayEnhancedResults(lastSimulationResults);
|
||||
|
||||
document.getElementById('resultsSection').style.display = 'block';
|
||||
|
||||
if (window.dashboard && candlesMap[interval]) {
|
||||
const chartData = candlesMap[interval].map(c => ({
|
||||
time: c.time,
|
||||
open: c.open,
|
||||
high: c.high,
|
||||
low: c.low,
|
||||
close: c.close
|
||||
}));
|
||||
window.dashboard.candleSeries.setData(chartData);
|
||||
window.dashboard.allData.set(interval, chartData);
|
||||
console.log(`Chart updated with ${chartData.length} candles from simulation range`);
|
||||
}
|
||||
|
||||
showSimulationMarkers();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Simulation error:', error);
|
||||
alert('Simulation error: ' + error.message);
|
||||
} finally {
|
||||
runBtn.disabled = false;
|
||||
runBtn.textContent = '▶ Run Simulation';
|
||||
}
|
||||
}
|
||||
|
||||
export function displayEnhancedResults(simulation) {
|
||||
const results = simulation.results || simulation;
|
||||
|
||||
document.getElementById('simTrades').textContent = results.total_trades || '0';
|
||||
document.getElementById('simWinRate').textContent = (results.win_rate || 0).toFixed(1) + '%';
|
||||
|
||||
const pnl = results.total_pnl || 0;
|
||||
const pnlElement = document.getElementById('simPnL');
|
||||
pnlElement.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2);
|
||||
pnlElement.style.color = pnl >= 0 ? '#4caf50' : '#f44336';
|
||||
|
||||
let grossProfit = 0;
|
||||
let grossLoss = 0;
|
||||
(results.trades || []).forEach(trade => {
|
||||
if (trade.pnl > 0) grossProfit += trade.pnl;
|
||||
else grossLoss += Math.abs(trade.pnl);
|
||||
});
|
||||
const profitFactor = grossLoss > 0 ? (grossProfit / grossLoss).toFixed(2) : grossProfit > 0 ? '∞' : '0';
|
||||
document.getElementById('simProfitFactor').textContent = profitFactor;
|
||||
|
||||
drawEquitySparkline(results);
|
||||
}
|
||||
|
||||
function drawEquitySparkline(results) {
|
||||
const container = document.getElementById('equitySparkline');
|
||||
if (!container || !results.trades || results.trades.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 11px;">No trades</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let equity = 1000;
|
||||
const equityData = [{ time: results.trades[0].entryTime, equity: equity }];
|
||||
|
||||
results.trades.forEach(trade => {
|
||||
equity += trade.pnl;
|
||||
equityData.push({ time: trade.exitTime, equity: equity });
|
||||
});
|
||||
|
||||
if (lastSimulationResults) {
|
||||
lastSimulationResults.equity_curve = equityData;
|
||||
}
|
||||
|
||||
container.innerHTML = '<canvas id="sparklineCanvas" width="300" height="60"></canvas>';
|
||||
const canvas = document.getElementById('sparklineCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const minEquity = Math.min(...equityData.map(d => d.equity));
|
||||
const maxEquity = Math.max(...equityData.map(d => d.equity));
|
||||
const range = maxEquity - minEquity || 1;
|
||||
|
||||
ctx.strokeStyle = equityData[equityData.length - 1].equity >= equityData[0].equity ? '#4caf50' : '#f44336';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
equityData.forEach((point, i) => {
|
||||
const x = (i / (equityData.length - 1)) * canvas.width;
|
||||
const y = canvas.height - ((point.equity - minEquity) / range) * canvas.height;
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#888';
|
||||
ctx.font = '9px sans-serif';
|
||||
ctx.fillText('$' + equityData[0].equity.toFixed(0), 2, canvas.height - 2);
|
||||
ctx.fillText('$' + equityData[equityData.length - 1].equity.toFixed(0), canvas.width - 30, 10);
|
||||
}
|
||||
|
||||
let tradeLineSeries = [];
|
||||
|
||||
export function showSimulationMarkers() {
|
||||
const results = getLastResults();
|
||||
if (!results || !window.dashboard) return;
|
||||
|
||||
const trades = results.trades || results.results?.trades || [];
|
||||
const markers = [];
|
||||
|
||||
clearSimulationMarkers();
|
||||
|
||||
console.log('Plotting trades:', trades.length);
|
||||
|
||||
trades.forEach((trade, i) => {
|
||||
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);
|
||||
}
|
||||
|
||||
const pnlSymbol = trade.pnl > 0 ? '+' : '';
|
||||
|
||||
markers.push({
|
||||
time: entryTime,
|
||||
position: 'belowBar',
|
||||
color: '#2196f3',
|
||||
shape: 'arrowUp',
|
||||
text: 'BUY',
|
||||
size: 1
|
||||
});
|
||||
|
||||
markers.push({
|
||||
time: exitTime,
|
||||
position: 'aboveBar',
|
||||
color: trade.pnl > 0 ? '#4caf50' : '#f44336',
|
||||
shape: 'arrowDown',
|
||||
text: `SELL ${pnlSymbol}${trade.pnlPct.toFixed(1)}%`,
|
||||
size: 1
|
||||
});
|
||||
|
||||
const lineSeries = window.dashboard.chart.addLineSeries({
|
||||
color: '#2196f3',
|
||||
lineWidth: 1,
|
||||
lastValueVisible: false,
|
||||
title: '',
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false
|
||||
});
|
||||
|
||||
lineSeries.setData([
|
||||
{ time: entryTime, value: trade.entryPrice },
|
||||
{ time: exitTime, value: trade.exitPrice }
|
||||
]);
|
||||
|
||||
tradeLineSeries.push(lineSeries);
|
||||
});
|
||||
|
||||
markers.sort((a, b) => a.time - b.time);
|
||||
|
||||
window.dashboard.candleSeries.setMarkers(markers);
|
||||
|
||||
console.log(`Plotted ${trades.length} trades with connection lines`);
|
||||
}
|
||||
|
||||
export function clearSimulationMarkers() {
|
||||
if (window.dashboard) {
|
||||
window.dashboard.candleSeries.setMarkers([]);
|
||||
|
||||
tradeLineSeries.forEach(series => {
|
||||
try {
|
||||
window.dashboard.chart.removeSeries(series);
|
||||
} catch (e) {
|
||||
// Series might already be removed
|
||||
}
|
||||
});
|
||||
tradeLineSeries = [];
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSimulationResults() {
|
||||
clearSimulationMarkers();
|
||||
|
||||
setLastResults(null);
|
||||
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
if (resultsSection) {
|
||||
resultsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const simTrades = document.getElementById('simTrades');
|
||||
const simWinRate = document.getElementById('simWinRate');
|
||||
const simPnL = document.getElementById('simPnL');
|
||||
const simProfitFactor = document.getElementById('simProfitFactor');
|
||||
const equitySparkline = document.getElementById('equitySparkline');
|
||||
|
||||
if (simTrades) simTrades.textContent = '0';
|
||||
if (simWinRate) simWinRate.textContent = '0%';
|
||||
if (simPnL) {
|
||||
simPnL.textContent = '$0.00';
|
||||
simPnL.style.color = '';
|
||||
}
|
||||
if (simProfitFactor) simProfitFactor.textContent = '0';
|
||||
if (equitySparkline) equitySparkline.innerHTML = '';
|
||||
}
|
||||
|
||||
function getStrategyConfig() {
|
||||
const strategyId = window.currentStrategy;
|
||||
if (!strategyId) return null;
|
||||
|
||||
const params = {};
|
||||
const paramDefs = window.StrategyParams?.[strategyId] || [];
|
||||
|
||||
paramDefs.forEach(def => {
|
||||
const input = document.getElementById(`param_${def.name}`);
|
||||
if (input) {
|
||||
params[def.name] = def.type === 'number' ? parseFloat(input.value) : input.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: strategyId,
|
||||
params: params
|
||||
};
|
||||
}
|
||||
47
src/api/dashboard/static/js/ui/storage.js
Normal file
47
src/api/dashboard/static/js/ui/storage.js
Normal file
@ -0,0 +1,47 @@
|
||||
export const SimulationStorage = {
|
||||
STORAGE_KEY: 'btc_bot_simulations',
|
||||
|
||||
getAll() {
|
||||
try {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch (e) {
|
||||
console.error('Error reading simulations:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
save(simulation) {
|
||||
try {
|
||||
const simulations = this.getAll();
|
||||
simulation.id = simulation.id || 'sim_' + Date.now();
|
||||
simulation.createdAt = new Date().toISOString();
|
||||
simulations.push(simulation);
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations));
|
||||
return simulation.id;
|
||||
} catch (e) {
|
||||
console.error('Error saving simulation:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
try {
|
||||
let simulations = this.getAll();
|
||||
simulations = simulations.filter(s => s.id !== id);
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(simulations));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Error deleting simulation:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
get(id) {
|
||||
return this.getAll().find(s => s.id === id);
|
||||
},
|
||||
|
||||
clear() {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
}
|
||||
};
|
||||
309
src/api/dashboard/static/js/ui/strategies-panel.js
Normal file
309
src/api/dashboard/static/js/ui/strategies-panel.js
Normal file
@ -0,0 +1,309 @@
|
||||
import { StrategyParams } from '../strategies/config.js';
|
||||
|
||||
let currentStrategy = null;
|
||||
|
||||
export function getCurrentStrategy() {
|
||||
return currentStrategy;
|
||||
}
|
||||
|
||||
export function setCurrentStrategy(strategyId) {
|
||||
currentStrategy = strategyId;
|
||||
window.currentStrategy = strategyId;
|
||||
}
|
||||
|
||||
export function renderStrategies(strategies) {
|
||||
const container = document.getElementById('strategyList');
|
||||
|
||||
if (!strategies || strategies.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px;">No strategies available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = strategies.map((s, index) => `
|
||||
<div class="strategy-item ${index === 0 ? 'selected' : ''}" data-strategy-id="${s.id}" onclick="selectStrategy('${s.id}')">
|
||||
<input type="radio" name="strategy" class="strategy-radio" ${index === 0 ? 'checked' : ''}>
|
||||
<span class="strategy-name">${s.name}</span>
|
||||
<span class="strategy-info" title="${s.description}">ⓘ</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
if (strategies.length > 0) {
|
||||
selectStrategy(strategies[0].id);
|
||||
}
|
||||
|
||||
document.getElementById('runSimBtn').disabled = false;
|
||||
}
|
||||
|
||||
export function selectStrategy(strategyId) {
|
||||
document.querySelectorAll('.strategy-item').forEach(item => {
|
||||
item.classList.toggle('selected', item.dataset.strategyId === strategyId);
|
||||
const radio = item.querySelector('input[type="radio"]');
|
||||
if (radio) radio.checked = item.dataset.strategyId === strategyId;
|
||||
});
|
||||
|
||||
setCurrentStrategy(strategyId);
|
||||
renderStrategyParams(strategyId);
|
||||
}
|
||||
|
||||
export function renderStrategyParams(strategyId) {
|
||||
const container = document.getElementById('strategyParams');
|
||||
const params = StrategyParams[strategyId] || [];
|
||||
|
||||
if (params.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = params.map(param => `
|
||||
<div class="config-group">
|
||||
<label class="config-label">${param.label}</label>
|
||||
<input type="${param.type}"
|
||||
id="param_${param.name}"
|
||||
class="config-input"
|
||||
value="${param.default}"
|
||||
${param.min !== undefined ? `min="${param.min}"` : ''}
|
||||
${param.max !== undefined ? `max="${param.max}"` : ''}
|
||||
${param.step !== undefined ? `step="${param.step}"` : ''}
|
||||
>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
export 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?_=' + Date.now(), {
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSimulation() {
|
||||
const results = getLastResults();
|
||||
if (!results) {
|
||||
alert('Please run a simulation first');
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultName = generateSimulationName(results.config);
|
||||
const name = prompt('Save simulation as:', defaultName);
|
||||
|
||||
if (!name || name.trim() === '') return;
|
||||
|
||||
const simulation = {
|
||||
name: name.trim(),
|
||||
config: results.config,
|
||||
results: {
|
||||
total_trades: results.total_trades,
|
||||
win_rate: results.win_rate,
|
||||
total_pnl: results.total_pnl,
|
||||
trades: results.trades,
|
||||
equity_curve: results.equity_curve
|
||||
}
|
||||
};
|
||||
|
||||
const id = window.SimulationStorage?.save(simulation);
|
||||
if (id) {
|
||||
renderSavedSimulations();
|
||||
alert('Simulation saved successfully!');
|
||||
} else {
|
||||
alert('Error saving simulation');
|
||||
}
|
||||
}
|
||||
|
||||
function generateSimulationName(config) {
|
||||
if (!config) return 'Unnamed Simulation';
|
||||
|
||||
const start = new Date(config.startDate);
|
||||
const now = new Date();
|
||||
const duration = now - start;
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
let dateStr;
|
||||
if (duration < oneDay) {
|
||||
dateStr = start.toISOString().slice(0, 16).replace('T', ' ');
|
||||
} else {
|
||||
dateStr = start.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
return `${config.strategyName}_${config.timeframe}_${dateStr}`;
|
||||
}
|
||||
|
||||
export function renderSavedSimulations() {
|
||||
const container = document.getElementById('savedSimulations');
|
||||
const simulations = window.SimulationStorage?.getAll() || [];
|
||||
|
||||
if (simulations.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">No saved simulations</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = simulations.map(sim => `
|
||||
<div class="saved-sim-item">
|
||||
<span class="saved-sim-name" onclick="loadSavedSimulation('${sim.id}')" title="${sim.name}">
|
||||
${sim.name.length > 25 ? sim.name.slice(0, 25) + '...' : sim.name}
|
||||
</span>
|
||||
<div class="saved-sim-actions">
|
||||
<button class="sim-action-btn" onclick="loadSavedSimulation('${sim.id}')" title="Load">📂</button>
|
||||
<button class="sim-action-btn" onclick="exportSavedSimulation('${sim.id}')" title="Export">📥</button>
|
||||
<button class="sim-action-btn" onclick="deleteSavedSimulation('${sim.id}')" title="Delete">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
export function loadSavedSimulation(id) {
|
||||
const sim = window.SimulationStorage?.get(id);
|
||||
if (!sim) {
|
||||
alert('Simulation not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sim.config) {
|
||||
document.getElementById('simSecondaryTF').value = sim.config.secondaryTimeframe || '';
|
||||
document.getElementById('simStartDate').value = sim.config.startDate || '';
|
||||
document.getElementById('simRiskPercent').value = sim.config.riskPercent || 2;
|
||||
document.getElementById('simStopLoss').value = sim.config.stopLossPercent || 2;
|
||||
|
||||
if (sim.config.strategyId) {
|
||||
selectStrategy(sim.config.strategyId);
|
||||
|
||||
if (sim.config.params) {
|
||||
Object.entries(sim.config.params).forEach(([key, value]) => {
|
||||
const input = document.getElementById(`param_${key}`);
|
||||
if (input) input.value = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLastResults(sim);
|
||||
displayEnhancedResults(sim.results);
|
||||
document.getElementById('resultsSection').style.display = 'block';
|
||||
}
|
||||
|
||||
export function deleteSavedSimulation(id) {
|
||||
if (!confirm('Are you sure you want to delete this simulation?')) return;
|
||||
|
||||
if (window.SimulationStorage?.delete(id)) {
|
||||
renderSavedSimulations();
|
||||
}
|
||||
}
|
||||
|
||||
function displayEnhancedResults(simulation) {
|
||||
const results = simulation.results || simulation;
|
||||
|
||||
document.getElementById('simTrades').textContent = results.total_trades || '0';
|
||||
document.getElementById('simWinRate').textContent = (results.win_rate || 0).toFixed(1) + '%';
|
||||
|
||||
const pnl = results.total_pnl || 0;
|
||||
const pnlElement = document.getElementById('simPnL');
|
||||
pnlElement.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2);
|
||||
pnlElement.style.color = pnl >= 0 ? '#4caf50' : '#f44336';
|
||||
|
||||
let grossProfit = 0;
|
||||
let grossLoss = 0;
|
||||
(results.trades || []).forEach(trade => {
|
||||
if (trade.pnl > 0) grossProfit += trade.pnl;
|
||||
else grossLoss += Math.abs(trade.pnl);
|
||||
});
|
||||
const profitFactor = grossLoss > 0 ? (grossProfit / grossLoss).toFixed(2) : grossProfit > 0 ? '∞' : '0';
|
||||
document.getElementById('simProfitFactor').textContent = profitFactor;
|
||||
|
||||
drawEquitySparkline(results);
|
||||
}
|
||||
|
||||
function drawEquitySparkline(results) {
|
||||
const container = document.getElementById('equitySparkline');
|
||||
if (!container || !results.trades || results.trades.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 11px;">No trades</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let equity = 1000;
|
||||
const equityData = [{ time: results.trades[0].entryTime, equity: equity }];
|
||||
|
||||
results.trades.forEach(trade => {
|
||||
equity += trade.pnl;
|
||||
equityData.push({ time: trade.exitTime, equity: equity });
|
||||
});
|
||||
|
||||
const sim = getLastResults();
|
||||
if (sim) {
|
||||
sim.equity_curve = equityData;
|
||||
}
|
||||
|
||||
container.innerHTML = '<canvas id="sparklineCanvas" width="300" height="60"></canvas>';
|
||||
const canvas = document.getElementById('sparklineCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const minEquity = Math.min(...equityData.map(d => d.equity));
|
||||
const maxEquity = Math.max(...equityData.map(d => d.equity));
|
||||
const range = maxEquity - minEquity || 1;
|
||||
|
||||
ctx.strokeStyle = equityData[equityData.length - 1].equity >= equityData[0].equity ? '#4caf50' : '#f44336';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
equityData.forEach((point, i) => {
|
||||
const x = (i / (equityData.length - 1)) * canvas.width;
|
||||
const y = canvas.height - ((point.equity - minEquity) / range) * canvas.height;
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#888';
|
||||
ctx.font = '9px sans-serif';
|
||||
ctx.fillText('$' + equityData[0].equity.toFixed(0), 2, canvas.height - 2);
|
||||
ctx.fillText('$' + equityData[equityData.length - 1].equity.toFixed(0), canvas.width - 30, 10);
|
||||
}
|
||||
|
||||
function getLastResults() {
|
||||
return window.lastSimulationResults;
|
||||
}
|
||||
|
||||
function setLastResults(results) {
|
||||
window.lastSimulationResults = results;
|
||||
}
|
||||
|
||||
window.selectStrategy = selectStrategy;
|
||||
window.loadSavedSimulation = loadSavedSimulation;
|
||||
window.deleteSavedSimulation = deleteSavedSimulation;
|
||||
window.renderSavedSimulations = renderSavedSimulations;
|
||||
23
src/api/dashboard/static/js/utils/helpers.js
Normal file
23
src/api/dashboard/static/js/utils/helpers.js
Normal file
@ -0,0 +1,23 @@
|
||||
export function downloadFile(content, filename, mimeType) {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
return new Date(date).toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
export function formatPrice(price, decimals = 2) {
|
||||
return price.toFixed(decimals);
|
||||
}
|
||||
|
||||
export function formatPercent(value) {
|
||||
return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
|
||||
}
|
||||
1
src/api/dashboard/static/js/utils/index.js
Normal file
1
src/api/dashboard/static/js/utils/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { downloadFile, formatDate, formatPrice, formatPercent } from './helpers.js';
|
||||
@ -5,34 +5,37 @@ Pure strategy logic separated from DB I/O for testability
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, asdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Optional, Any, List
|
||||
import importlib
|
||||
from typing import Dict, Optional, Any, List, Callable
|
||||
|
||||
from .database import DatabaseManager
|
||||
from .indicator_engine import IndicatorEngine
|
||||
from src.strategies.base import BaseStrategy, StrategySignal, SignalType
|
||||
from src.strategies.ma_strategy import MAStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Registry of available strategies
|
||||
STRATEGY_REGISTRY = {
|
||||
"ma44_strategy": "src.strategies.ma44_strategy.MA44Strategy",
|
||||
"ma125_strategy": "src.strategies.ma125_strategy.MA125Strategy",
|
||||
def _create_ma44() -> BaseStrategy:
|
||||
return MAStrategy(config={"period": 44})
|
||||
|
||||
def _create_ma125() -> BaseStrategy:
|
||||
return MAStrategy(config={"period": 125})
|
||||
|
||||
STRATEGY_REGISTRY: Dict[str, Callable[[], BaseStrategy]] = {
|
||||
"ma_trend": MAStrategy,
|
||||
"ma44_strategy": _create_ma44,
|
||||
"ma125_strategy": _create_ma125,
|
||||
}
|
||||
|
||||
def load_strategy(strategy_name: str) -> BaseStrategy:
|
||||
"""Dynamically load a strategy class"""
|
||||
"""Load a strategy instance from registry"""
|
||||
if strategy_name not in STRATEGY_REGISTRY:
|
||||
# Default fallback or error
|
||||
logger.warning(f"Strategy {strategy_name} not found, defaulting to MA44")
|
||||
strategy_name = "ma44_strategy"
|
||||
logger.warning(f"Strategy {strategy_name} not found, defaulting to ma_trend")
|
||||
strategy_name = "ma_trend"
|
||||
|
||||
module_path, class_name = STRATEGY_REGISTRY[strategy_name].rsplit('.', 1)
|
||||
module = importlib.import_module(module_path)
|
||||
cls = getattr(module, class_name)
|
||||
return cls()
|
||||
factory = STRATEGY_REGISTRY[strategy_name]
|
||||
return factory()
|
||||
|
||||
@dataclass
|
||||
class Decision:
|
||||
|
||||
@ -19,6 +19,7 @@ from .database import DatabaseManager
|
||||
from .custom_timeframe_generator import CustomTimeframeGenerator
|
||||
from .indicator_engine import IndicatorEngine, IndicatorConfig
|
||||
from .brain import Brain
|
||||
from .backfill import HyperliquidBackfill
|
||||
|
||||
|
||||
# Configure logging
|
||||
@ -267,7 +268,7 @@ class DataCollector:
|
||||
gaps = await self.db.detect_gaps(self.symbol, self.interval)
|
||||
if gaps:
|
||||
logger.warning(f"Detected {len(gaps)} data gaps: {gaps}")
|
||||
# Could trigger backfill here
|
||||
await self._backfill_gaps(gaps)
|
||||
|
||||
# Log database health
|
||||
health = await self.db.get_health_stats()
|
||||
@ -278,6 +279,36 @@ class DataCollector:
|
||||
except Exception as e:
|
||||
logger.error(f"Error in monitoring loop: {e}")
|
||||
|
||||
async def _backfill_gaps(self, gaps: list) -> None:
|
||||
"""Backfill detected data gaps from Hyperliquid"""
|
||||
if not gaps:
|
||||
return
|
||||
|
||||
logger.info(f"Starting backfill for {len(gaps)} gaps...")
|
||||
|
||||
try:
|
||||
async with HyperliquidBackfill(self.db, self.symbol, [self.interval]) as backfill:
|
||||
for gap in gaps:
|
||||
gap_start = datetime.fromisoformat(gap['gap_start'].replace('Z', '+00:00'))
|
||||
gap_end = datetime.fromisoformat(gap['gap_end'].replace('Z', '+00:00'))
|
||||
|
||||
logger.info(f"Backfilling gap: {gap_start} to {gap_end} ({gap['missing_candles']} candles)")
|
||||
|
||||
candles = await backfill.fetch_candles(self.interval, gap_start, gap_end)
|
||||
|
||||
if candles:
|
||||
inserted = await self.db.insert_candles(candles)
|
||||
logger.info(f"Backfilled {inserted} candles for gap {gap_start}")
|
||||
|
||||
# Update custom timeframes and indicators for backfilled data
|
||||
if inserted > 0:
|
||||
await self._update_custom_timeframes(candles)
|
||||
else:
|
||||
logger.warning(f"No candles available for gap {gap_start} to {gap_end}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Backfill failed: {e}")
|
||||
|
||||
def _setup_signal_handlers(self) -> None:
|
||||
"""Setup handlers for graceful shutdown"""
|
||||
def signal_handler(sig, frame):
|
||||
|
||||
Reference in New Issue
Block a user