Pre-refactor: commit before converting indicators to self-contained files

This commit is contained in:
DiTus
2026-03-01 19:37:07 +01:00
parent e457ce3e20
commit fdab0a3faa
22 changed files with 96 additions and 2726 deletions

View File

@ -1392,7 +1392,6 @@
<div class="sidebar-header">
<div class="sidebar-tabs">
<button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button>
<button class="sidebar-tab" data-tab="strategies">📋 Strategies</button>
</div>
<button class="sidebar-toggle" id="sidebarToggleBtn"></button>
</div>
@ -1406,113 +1405,6 @@
</div>
</div>
</div>
<!-- Strategies Tab -->
<div class="sidebar-tab-panel" id="tab-strategies">
<!-- Strategy Selection -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>📋</span> Select Strategy
</div>
<div class="sidebar-section-content" id="strategyList">
<div class="loading-strategies" style="text-align: center; color: var(--tv-text-secondary); padding: 20px;">
Loading strategies...
</div>
</div>
</div>
<!-- Configuration -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>⚙️</span> Configuration
</div>
<div class="sidebar-section-content">
<div class="config-group">
<label class="config-label">Start Date</label>
<input type="datetime-local" id="simStartDate" class="config-input">
</div>
<div class="config-group">
<label class="config-label">Confirmation TF (Optional)</label>
<select id="simSecondaryTF" class="config-input">
<option value="">None</option>
<option value="1h">1h</option>
<option value="4h">4h</option>
<option value="1d" selected>1d</option>
<option value="1w">1w</option>
</select>
</div>
<div class="config-group">
<label class="config-label">Risk % per Trade</label>
<input type="number" id="simRiskPercent" class="config-input" value="2" min="0.1" max="100" step="0.1">
</div>
<div class="config-group">
<label class="config-label">Stop Loss %</label>
<input type="number" id="simStopLoss" class="config-input" value="2" min="0.1" max="20" step="0.1">
</div>
<div id="strategyParams"></div>
</div>
</div>
<!-- Run Button -->
<button class="action-btn primary" onclick="runSimulation()" id="runSimBtn" disabled>
▶ Run Simulation
</button>
<!-- Results Section -->
<div class="sidebar-section" id="resultsSection" style="display: none;">
<div class="sidebar-section-header">
<span>📊</span> Results
</div>
<div class="sidebar-section-content">
<div class="equity-sparkline" id="equitySparkline"></div>
<div class="results-summary">
<div class="result-stat">
<div class="result-stat-value" id="simTrades">--</div>
<div class="result-stat-label">Trades</div>
</div>
<div class="result-stat">
<div class="result-stat-value" id="simWinRate">--</div>
<div class="result-stat-label">Win Rate</div>
</div>
<div class="result-stat">
<div class="result-stat-value" id="simPnL">--</div>
<div class="result-stat-label">Total P&L</div>
</div>
<div class="result-stat">
<div class="result-stat-value" id="simProfitFactor">--</div>
<div class="result-stat-label">Profit Factor</div>
</div>
</div>
<button class="action-btn secondary" onclick="showSimulationMarkers()">
📍 Plot on Chart
</button>
<button class="action-btn secondary" onclick="saveSimulation()">
💾 Save Simulation
</button>
<button class="action-btn success" onclick="showExportDialog()">
📥 Export Report
</button>
</div>
</div>
<!-- Saved Simulations -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>💾</span> Saved Simulations
</div>
<div class="sidebar-section-content" id="savedSimulations">
<div style="text-align: center; color: var(--tv-text-secondary); padding: 10px; font-size: 12px;">
No saved simulations
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,25 +1,5 @@
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
import { restoreSidebarState, toggleSidebar, initSidebarTabs, restoreSidebarTabState } 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 {
initIndicatorPanel,
getActiveIndicators,
@ -28,42 +8,13 @@ import {
addIndicator,
removeIndicatorById
} from './ui/indicators-panel-new.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.showSimulationMarkers = showSimulationMarkers;
window.renderSavedSimulations = renderSavedSimulations;
window.loadSavedSimulation = loadSavedSimulation;
window.deleteSavedSimulation = deleteSavedSimulation;
window.clearSimulationResults = clearSimulationResults;
window.updateTimeframeDisplay = updateTimeframeDisplay;
window.renderIndicatorList = function() {
// This function is no longer needed for sidebar indicators
};
@ -73,8 +24,6 @@ window.initIndicatorPanel = initIndicatorPanel;
window.addIndicator = addIndicator;
window.toggleIndicator = addIndicator;
window.StrategyParams = StrategyParams;
window.SimulationStorage = SimulationStorage;
window.IndicatorRegistry = IndicatorRegistry;
document.addEventListener('DOMContentLoaded', async () => {
@ -88,11 +37,6 @@ document.addEventListener('DOMContentLoaded', async () => {
restoreSidebarState();
restoreSidebarTabState();
initSidebarTabs();
setDefaultStartDate();
updateTimeframeDisplay();
renderSavedSimulations();
await loadStrategies();
// Initialize indicator panel
window.initIndicatorPanel();

View File

@ -1,12 +0,0 @@
export const StrategyParams = {
hts_trend: [
{ name: 'shortPeriod', label: 'Fast Period', type: 'number', default: 33, min: 5, max: 200 },
{ name: 'longPeriod', label: 'Slow Period', type: 'number', default: 144, min: 10, max: 500 },
{ name: 'maType', label: 'MA Type', type: 'select', options: ['RMA', 'SMA', 'EMA', 'WMA', 'VWMA'], default: 'RMA' },
{ name: 'useAutoHTS', label: 'Auto HTS (TF/4)', type: 'boolean', default: false },
{ name: 'use1HFilter', label: '1H Red Zone Filter', type: 'boolean', default: true }
],
ma_trend: [
{ name: 'period', label: 'MA Period', type: 'number', default: 44, min: 5, max: 500 }
]
};

View File

@ -1,167 +0,0 @@
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';
}
}

View File

@ -1,423 +0,0 @@
import { MA } from '../indicators/ma.js';
/**
* HTS (Higher Timeframe Trend System) Strategy Engine
* Computes trading signals based on HTS indicator rules:
* 1. Trend detection using fast/slow channels
* 2. Entry rules on channel breakouts
* 3. 1H timeframe filter (optional)
* 4. Stop loss based on opposite channel
*/
export class HTSStrategyEngine {
constructor(config = {}) {
this.config = {
shortPeriod: config.shortPeriod || 33,
longPeriod: config.longPeriod || 144,
maType: config.maType || 'RMA',
useAutoHTS: config.useAutoHTS || false,
use1HFilter: config.use1HFilter !== false
};
}
calculateHTSCandles(candles, periodMult = 1) {
if (!candles || candles.length < this.config.longPeriod) return [];
const shortPeriod = this.config.shortPeriod;
const longPeriod = this.config.longPeriod;
const maType = this.config.maType;
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');
const results = [];
for (let i = 0; i < candles.length; i++) {
if (!shortHigh[i] || !longLow[i]) continue;
results.push({
time: candles[i].time,
fastHigh: shortHigh[i],
fastLow: shortLow[i],
slowHigh: longHigh[i],
slowLow: longLow[i],
price: candles[i].close,
fastMidpoint: (shortHigh[i] + shortLow[i]) / 2,
slowMidpoint: (longHigh[i] + longLow[i]) / 2
});
}
return results;
}
computeAutoHTS(oneMinCandles, targetTF) {
if (!oneMinCandles || oneMinCandles.length < this.config.longPeriod) return [];
const tfMultipliers = {
'5m': 5,
'15m': 15,
'30m': 30,
'37m': 37,
'1h': 60,
'4h': 240
};
const tfGroup = tfMultipliers[targetTF] || 5;
const grouped = [];
let currentGroup = [];
for (let i = 0; i < oneMinCandles.length; i++) {
currentGroup.push(oneMinCandles[i]);
if (currentGroup.length >= tfGroup) {
grouped.push(currentGroup[tfGroup - 1]);
currentGroup = [];
}
}
return this.calculateHTSCandles(grouped);
}
check1HFilter(price, h1hHTS) {
if (!this.config.use1HFilter || !h1hHTS || h1hHTS.length === 0) {
return { confirmed: true, reasoning: '1H filter disabled or no data' };
}
const latest = h1hHTS[h1hHTS.length - 1];
const slowLow = latest?.slowLow;
if (slowLow === null || slowLow === undefined) {
return { confirmed: true, reasoning: '1H HTS not ready' };
}
if (price < slowLow) {
return {
confirmed: false,
reasoning: `1H Filter: Long rejected - price ${price.toFixed(2)} below 1H slow channel (${slowLow.toFixed(2)})`
};
}
return {
confirmed: true,
reasoning: `1H Filter: Confirmed - price ${price.toFixed(2)} above 1H slow channel (${slowLow.toFixed(2)})`
};
}
calculateEntrySignal(primaryHTS, index, position, h1hHTS = null, prevHTS = null) {
if (!primaryHTS || index >= primaryHTS.length || index < 1) {
return { signal: 'HOLD', confidence: 0, reasoning: 'Insufficient data' };
}
const current = primaryHTS[index];
const currentPrice = current?.price;
if (!currentPrice) {
return { signal: 'HOLD', confidence: 0, reasoning: 'No price data' };
}
const prev = primaryHTS[index - 1];
const h1hCheck = this.check1HFilter(currentPrice, h1hHTS);
if (!h1hCheck.confirmed) {
return {
signal: 'HOLD',
confidence: 0,
reasoning: h1hCheck.reasoning,
filterStatus: '1H_REJECTED'
};
}
const fastLow = current?.fastLow;
const fastHigh = current?.fastHigh;
const slowLow = current?.slowLow;
const slowHigh = current?.slowHigh;
const prevFastLow = prev?.fastLow;
const prevSlowLow = prev?.slowLow;
const prevFastHigh = prev?.fastHigh;
const prevSlowHigh = prev?.slowHigh;
if (!fastLow || !slowLow) {
return { signal: 'HOLD', confidence: 0, reasoning: 'HTS data not ready' };
}
const inPositionLong = position?.type === 'long';
const inPositionShort = position?.type === 'short';
if (inPositionLong) {
if (currentPrice < slowLow || fastLow < slowLow) {
return {
signal: 'CLOSE_LONG',
confidence: 90,
reasoning: `Stop Loss: Price ${currentPrice.toFixed(2)} broke below slow channel (${slowLow.toFixed(2)})`,
stopPrice: slowLow
};
}
return {
signal: 'HOLD',
confidence: 50,
reasoning: `Long open, holding - price ${currentPrice.toFixed(2)} above stop at ${slowLow.toFixed(2)}`,
stopPrice: slowLow
};
}
if (inPositionShort) {
if (currentPrice > slowHigh || fastHigh > slowHigh) {
return {
signal: 'CLOSE_SHORT',
confidence: 90,
reasoning: `Stop Loss: Price ${currentPrice.toFixed(2)} broke above slow channel (${slowHigh.toFixed(2)})`,
stopPrice: slowHigh
};
}
return {
signal: 'HOLD',
confidence: 50,
reasoning: `Short open, holding - price ${currentPrice.toFixed(2)} below stop at ${slowHigh.toFixed(2)}`,
stopPrice: slowHigh
};
}
if (prevFastLow <= prevSlowLow && fastLow > slowLow) {
return {
signal: 'BUY',
confidence: 75,
reasoning: `Long Entry: Fast Low (${fastLow.toFixed(2)}) crossed above Slow Low (${slowLow.toFixed(2)})`,
entryType: 'CROSSOVER'
};
}
if (prevFastHigh >= prevSlowHigh && fastHigh < slowHigh) {
return {
signal: 'SELL',
confidence: 75,
reasoning: `Short Entry: Fast High (${fastHigh.toFixed(2)}) crossed below Slow High (${slowHigh.toFixed(2)})`,
entryType: 'CROSSUNDER'
};
}
const bullAlignment = slowLow < slowHigh && fastLow > slowLow;
const bearAlignment = slowLow < slowHigh && fastHigh < slowHigh;
if (bullAlignment && currentPrice > slowLow) {
return {
signal: 'BUY',
confidence: 50,
reasoning: `Long Entry: Price ${currentPrice.toFixed(2)} above Slow Low with bullish channel alignment`,
entryType: 'ALIGNED'
};
}
if (bearAlignment && currentPrice < slowHigh) {
return {
signal: 'SELL',
confidence: 50,
reasoning: `Short Entry: Price ${currentPrice.toFixed(2)} below Slow High with bearish channel alignment`,
entryType: 'ALIGNED'
};
}
return {
signal: 'HOLD',
confidence: 10,
reasoning: 'No clear signal - waiting for crossover'
};
}
computeStopLoss(position, currentHTS, index) {
if (!position || !currentHTS || index >= currentHTS.length) return null;
const current = currentHTS[index];
if (!current) return null;
if (position.type === 'long') {
return current.slowLow;
}
if (position.type === 'short') {
return current.slowHigh;
}
return null;
}
calculateHTSCandles(candles, periodMult = 1) {
if (!candles || candles.length < this.config.longPeriod) return [];
const shortPeriod = this.config.shortPeriod;
const longPeriod = this.config.longPeriod;
const maType = this.config.maType;
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');
const results = [];
for (let i = 0; i < candles.length; i++) {
if (!shortHigh[i] || !longLow[i]) continue;
results.push({
time: candles[i].time,
fastHigh: shortHigh[i],
fastLow: shortLow[i],
slowHigh: longHigh[i],
slowLow: longLow[i],
price: candles[i].close,
fastMidpoint: ((shortHigh[i] + shortLow[i]) / 2),
slowMidpoint: ((longHigh[i] + longLow[i]) / 2),
open: candles[i].open,
high: candles[i].high,
low: candles[i].low,
close: candles[i].close,
volume: candles[i].volume
});
}
return results;
}
runSimulation(candles, oneMinCandles, h1hCandles) {
const primaryHTS = this.calculateHTSCandles(candles);
console.log('[HTS] Starting simulation');
console.log('[HTS] Primary HTS data length:', primaryHTS.length);
console.log('[HTS] Original candles length:', candles.length);
let h1hHTS = null;
if (h1hCandles && h1hCandles.length > 0) {
h1hHTS = this.calculateHTSCandles(h1hCandles);
console.log('[HTS] 1H HTS data length:', h1hHTS.length);
}
let position = null;
const trades = [];
let balance = 1000;
let rejectionsCount = 0;
let buySignals = 0;
let sellSignals = 0;
for (let i = 1; i < primaryHTS.length; i++) {
if (!primaryHTS[i]) continue;
const htsData = primaryHTS[i];
const signal = this.calculateEntrySignal(primaryHTS, i, position, h1hHTS, primaryHTS[i - 1]);
if (signal.filterStatus === '1H_REJECTED') {
rejectionsCount++;
continue;
}
if (signal.signal === 'BUY' && !position) {
buySignals++;
console.log('[HTS] BUY SIGNAL:', signal.reasoning);
position = {
type: 'long',
entryPrice: htsData.price,
entryTime: htsData.time,
size: 1,
stopLoss: signal.stopPrice || primaryHTS[i].slowLow
};
console.log('[HTS] LONG entry at', htsData.price, 'stop at', position.stopLoss);
}
if (signal.signal === 'SELL' && !position) {
sellSignals++;
console.log('[HTS] SELL SIGNAL:', signal.reasoning);
position = {
type: 'short',
entryPrice: htsData.price,
entryTime: htsData.time,
size: 1,
stopLoss: signal.stopPrice || primaryHTS[i].slowHigh
};
console.log('[HTS] SHORT entry at', htsData.price, 'stop at', position.stopLoss);
}
if ((signal.signal === 'CLOSE_LONG' || signal.signal === 'CLOSE_SHORT') && position) {
const pnl = position.type === 'long'
? (htsData.price - position.entryPrice)
: (position.entryPrice - htsData.price);
trades.push({
...position,
exitPrice: htsData.price,
exitTime: htsData.time,
pnl,
pnlPct: (pnl / position.entryPrice) * 100
});
balance += pnl;
console.log('[HTS] Position closed:', signal.signal, 'PnL:', pnl.toFixed(2));
position = null;
}
if (position) {
const sl = this.computeStopLoss(position, primaryHTS, i);
if (sl !== null) {
const slHit = position.type === 'long' ? htsData.price < sl : htsData.price > sl;
if (slHit) {
const pnl = position.type === 'long'
? (sl - position.entryPrice)
: (position.entryPrice - sl);
trades.push({
...position,
exitPrice: sl,
exitTime: htsData.time,
pnl,
pnlPct: (pnl / position.entryPrice) * 100,
exitReason: 'STOP_LOSS'
});
balance += pnl;
console.log('[HTS] Stop Loss hit, PnL:', pnl.toFixed(2));
position = null;
}
}
}
}
const winRate = trades.length > 0
? (trades.filter(t => t.pnl > 0).length / trades.length) * 100
: 0;
console.log('[HTS] Simulation complete:');
console.log('[HTS] - Buy signals:', buySignals);
console.log('[HTS] - Sell signals:', sellSignals);
console.log('[HTS] - 1H filter rejections:', rejectionsCount);
console.log('[HTS] - Total HTS candles processed:', primaryHTS.length);
console.log('[HTS] - Total trades:', trades.length);
console.log('[HTS] - Final balance:', balance.toFixed(2));
console.log('[HTS] - Win rate:', winRate.toFixed(1) + '%');
if (trades.length === 0) {
console.warn('[HTS] No trades generated! Possible reasons:');
if (rejectionsCount > 0) {
console.warn(` - 1H filter rejected ${rejectionsCount} potential trades`);
}
if (buySignals === 0 && sellSignals === 0) {
console.warn(' - No crossover or alignment signals detected');
console.warn(' - Market may be in ranging/sideways mode');
}
console.warn(' - Consider:');
console.warn(' * Reducing 1H filter strictness or disabling it');
console.warn(' * Adjusting short/long periods for current timeframe');
console.warn(' * Checking if data has sufficient price movement');
}
return {
trades,
balance,
totalTrades: trades.length,
winRate,
finalPnL: balance - 1000,
total_trades: trades.length,
win_rate: winRate,
total_pnl: balance - 1000,
htsData: primaryHTS
};
}
}

View File

@ -1,3 +0,0 @@
export { StrategyParams } from './config.js';
export { RiskManager } from './risk-manager.js';
export { ClientStrategyEngine } from './engine.js';

View File

@ -1,17 +0,0 @@
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;
}
}

View File

@ -1,140 +0,0 @@
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;

View File

@ -1,5 +1,3 @@
import { HTSStrategyEngine } from '../strategies/hts-engine.js';
const HTS_COLORS = {
fastHigh: '#00bcd4',
fastLow: '#00bcd4',

View File

@ -1,28 +1,5 @@
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,

View File

@ -298,6 +298,47 @@ function renderIndicatorConfig(indicator, meta) {
` : ''}
</div>
${window.dashboard?.indicatorSignals ? `
<div class="config-section">
<div class="section-subtitle">Signal Status</div>
${(() => {
const indSignal = window.dashboard.indicatorSignals.find(s => s.id === indicator.id);
if (!indSignal) {
return `<div class="config-row" style="color: var(--tv-text-secondary); font-size: 12px;">No signal data available</div>`;
}
const signalType = indSignal.signal.toUpperCase();
const signalIcon = signalType === 'BUY' ? '🟢' : signalType === 'SELL' ? '🔴' : '⚪';
const signalColor = indSignal.color || 'var(--tv-text-secondary)';
const signalDate = indSignal.lastSignalDate ? new Date(indSignal.lastSignalDate * 1000).toLocaleString() : 'Waiting for crossover...';
return `
<div class="config-row" style="align-items: center;">
<label style="flex: 0 0 80px;">Status</label>
<span class="ta-signal ${indSignal.signal}" style="font-size: 12px; padding: 4px 12px; min-width: 70px; text-align: center; background: ${signalColor}; color: white; border-radius: 4px;">
${signalIcon} ${signalType}
</span>
</div>
<div class="config-row">
<label style="flex: 0 0 80px;">Last Signal</label>
<span style="color: var(--tv-text-primary); font-size: 12px;">${signalDate}</span>
</div>
<div class="config-row">
<label style="flex: 0 0 80px;">Strength</label>
<div style="display: flex; align-items: center; gap: 8px; flex: 1;">
<input type="range" min="0" max="100" value="${indSignal.strength}" disabled style="width: 80px;">
<span style="color: var(--tv-text-primary); font-size: 12px;">${indSignal.strength}%</span>
</div>
</div>
<div class="config-row">
<label style="flex: 0 0 80px;">Reasoning</label>
<span style="color: var(--tv-text-primary); font-size: 11px;">${indSignal.reasoning}</span>
</div>
`;
})()}
</div>
` : ''}
${meta?.inputs && meta.inputs.length > 0 ? `
<div class="config-section">
<div class="section-subtitle">Parameters</div>

View File

@ -236,18 +236,16 @@ function calculateMASignal(indicator, lastCandle, prevCandle, values) {
let signalType, strength, reasoning;
if (close > ma * 1.02) {
if (close > ma) {
signalType = SIGNAL_TYPES.BUY;
strength = Math.min(60 + ((close - ma) / ma) * 500, 100);
reasoning = `Price (${close.toFixed(2)}) is strongly above ${maLabel} (${ma.toFixed(2)}), bullish trend`;
} else if (close < ma * 0.98) {
reasoning = `Price (${close.toFixed(2)}) is above ${maLabel} (${ma.toFixed(2)})`;
} else if (close < ma) {
signalType = SIGNAL_TYPES.SELL;
strength = Math.min(60 + ((ma - close) / ma) * 500, 100);
reasoning = `Price (${close.toFixed(2)}) is strongly below ${maLabel} (${ma.toFixed(2)}), bearish trend`;
reasoning = `Price (${close.toFixed(2)}) is below ${maLabel} (${ma.toFixed(2)})`;
} else {
signalType = SIGNAL_TYPES.HOLD;
strength = 30;
reasoning = `Price (${close.toFixed(2)}) is near ${maLabel} (${ma.toFixed(2)}), sideways/consolidating`;
return null;
}
console.log('[calculateMASignal] Result:', signalType, strength);
@ -406,6 +404,36 @@ export function calculateAllIndicatorSignals() {
const signal = calculateIndicatorSignal(indicator, candles, values);
let currentSignal = signal;
let lastSignalDate = indicator.lastSignalTimestamp || null;
let lastSignalType = indicator.lastSignalType || null;
if (!currentSignal || !currentSignal.type) {
console.log('[Signals] No valid signal for', indicator.type, '- Using last signal if available');
if (lastSignalType && lastSignalDate) {
currentSignal = {
type: lastSignalType,
strength: 50,
value: candles[candles.length - 1]?.close,
reasoning: `No crossover (price equals MA)`
};
} else {
console.log('[Signals] No previous signal available - Skipping');
continue;
}
} else {
const currentCandleTimestamp = candles[candles.length - 1].time;
if (currentSignal.type !== lastSignalType || !lastSignalType) {
console.log('[Signals] Signal changed for', indicator.type, ':', lastSignalType, '->', currentSignal.type);
lastSignalDate = currentCandleTimestamp;
lastSignalType = currentSignal.type;
indicator.lastSignalTimestamp = lastSignalDate;
indicator.lastSignalType = lastSignalType;
}
}
const label = indicator.type?.toUpperCase();
const params = indicator.params && typeof indicator.params === 'object'
? Object.entries(indicator.params)
@ -420,11 +448,12 @@ export function calculateAllIndicatorSignals() {
label: label,
params: params || null,
type: indicator.type,
signal: signal.type,
strength: Math.round(signal.strength),
value: signal.value,
reasoning: signal.reasoning,
color: SIGNAL_COLORS[signal.type]
signal: currentSignal.type,
strength: Math.round(currentSignal.strength),
value: currentSignal.value,
reasoning: currentSignal.reasoning,
color: SIGNAL_COLORS[currentSignal.type],
lastSignalDate: lastSignalDate
});
}

View File

@ -1,567 +0,0 @@
import { ClientStrategyEngine } from '../strategies/index.js';
import { HTSStrategyEngine } from '../strategies/hts-engine.js';
import { addHTSVisualization } from './hts-visualizer.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));
let results;
if (strategyConfig.id === 'hts_trend') {
console.log('Running HTS strategy simulation...');
const htsEngine = new HTSStrategyEngine(strategyConfig.params);
let oneMinCandles = null;
let h1hCandles = null;
if (strategyConfig.params?.useAutoHTS) {
console.log('Fetching 1m candles for Auto HTS...');
const oneMinQuery = `symbol=BTC&interval=1m&start=${fetchStart.toISOString()}&limit=10000`;
const oneMinResponse = await fetch(`/api/v1/candles?${oneMinQuery}`);
if (oneMinResponse.ok) {
const oneMinData = await oneMinResponse.json();
if (oneMinData.candles && oneMinData.candles.length > 0) {
oneMinCandles = oneMinData.candles.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),
volume: parseFloat(c.volume)
}));
console.log(`Got ${oneMinCandles.length} 1m candles`);
}
}
}
if (strategyConfig.params?.use1HFilter) {
console.log('Fetching 1H candles for HTS 1H filter...');
const h1hQuery = `symbol=BTC&interval=1h&start=${fetchStart.toISOString()}&limit=500`;
const h1hResponse = await fetch(`/api/v1/candles?${h1hQuery}`);
if (h1hResponse.ok) {
const h1hData = await h1hResponse.json();
if (h1hData.candles && h1hData.candles.length > 0) {
h1hCandles = h1hData.candles.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),
volume: parseFloat(c.volume)
}));
console.log(`Got ${h1hCandles.length} 1H candles`);
}
}
}
results = htsEngine.runSimulation(candlesMap[interval], oneMinCandles, h1hCandles);
} else {
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();
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();
if (strategyConfig.id === 'hts_trend' && results.htsData && window.dashboard) {
try {
console.log('Visualizing HTS channels...');
const candles = window.dashboard.allData.get(interval) || candlesMap[interval];
htsVisualizer = addHTSVisualization(
window.dashboard.chart,
window.dashboard.candleSeries,
results.htsData,
candles,
strategyConfig.params?.useAutoHTS
);
console.log('HTS channels visualized');
} catch (e) {
console.error('Error visualizing HTS channels:', e);
}
}
} 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 = [];
let htsVisualizer = null;
let tradeMarkerSeries = [];
export function showSimulationMarkers() {
const results = getLastResults();
if (!results || !window.dashboard) {
console.warn('Cannot show markers: no results or dashboard');
return;
}
const trades = results.trades || results.results?.trades || [];
const markers = [];
clearSimulationMarkers();
console.log('Plotting trades:', trades.length);
console.log('Dashboard check:', {
dashboard: !!window.dashboard,
chart: !!window.dashboard?.chart,
candleSeries: !!window.dashboard?.candleSeries,
setMarkers: typeof window.dashboard?.candleSeries?.setMarkers
});
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);
}
console.log(`[Trade ${i}] Entry: ${new Date(entryTime * 1000).toISOString()}, Exit: ${new Date(exitTime * 1000).toISOString()}`);
console.log(` Entry Price: $${trade.entryPrice.toFixed(2)} | Exit Price: $${trade.exitPrice.toFixed(2)} | PnL: ${trade.pnl > 0 ? '+' : ''}$${trade.pnl.toFixed(2)} (${trade.pnlPct.toFixed(1)}%)`);
if (window.dashboard && window.dashboard.chart && window.dashboard.candleSeries) {
try {
const data = window.dashboard.candleSeries.data();
if (!data || data.length === 0) {
console.warn('No chart data available for markers');
return;
}
const chartTimeRange = {
min: Math.min(...data.map(c => c.time)),
max: Math.max(...data.map(c => c.time))
};
if (entryTime < chartTimeRange.min || entryTime > chartTimeRange.max) {
console.log(`Skipping trade ${i} - entry time outside chart range`);
return;
}
if (exitTime < chartTimeRange.min || exitTime > chartTimeRange.max) {
console.log(`Skipping trade ${i} - exit time outside chart range`);
return;
}
const buyMarkerSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: '#2196f3',
lineWidth: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false
});
const entryPrice = trade.entryPrice;
buyMarkerSeries.setData([
{ time: entryTime, value: entryPrice },
{ time: entryTime + 1, value: entryPrice }
]);
tradeMarkerSeries.push({ series: buyMarkerSeries });
const pnlColor = trade.pnl > 0 ? '#4caf50' : '#f44336';
const sellMarkerSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: pnlColor,
lineWidth: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false
});
const exitPrice = trade.exitPrice;
sellMarkerSeries.setData([
{ time: exitTime, value: exitPrice },
{ time: exitTime + 1, value: exitPrice }
]);
tradeMarkerSeries.push({ series: sellMarkerSeries });
const lineSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: 'rgba(33, 150, 243, 0.3)',
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false
});
lineSeries.setData([
{ time: entryTime, value: trade.entryPrice },
{ time: exitTime, value: trade.exitPrice }
]);
tradeLineSeries.push(lineSeries);
} catch (e) {
console.error(`Error adding marker for trade ${i}:`, e.message);
}
}
});
markers.sort((a, b) => a.time - b.time);
const candleSeries = window.dashboard.candleSeries;
if (candleSeries && typeof candleSeries.setMarkers === 'function') {
try {
console.log('Setting', markers.length, 'markers on candle series');
const currentData = candleSeries.data();
const markerTimes = new Set(markers.map(m => m.time));
const candleTimes = new Set(currentData.map(c => c.time));
const orphanMarkers = markers.filter(m => !candleTimes.has(m.time));
if (orphanMarkers.length > 0) {
console.warn(`${orphanMarkers.length} markers have timestamps not found in chart data:`);
orphanMarkers.forEach(m => {
console.warn(' - Time:', m.time, '(', new Date(m.time * 1000).toISOString(), ')');
});
console.log('First few candle times:', Array.from(candleTimes).slice(0, 5));
console.log('Last few candle times:', Array.from(candleTimes).slice(-5));
}
candleSeries.setMarkers(markers);
console.log('Markers set successfully');
} catch (e) {
console.error('Error setting markers:', e);
console.error('Marker methods available:', Object.getOwnPropertyNames(Object.getPrototypeOf(candleSeries)).filter(m => m.includes('marker')));
}
} else {
console.warn('Markers not supported on this chart version - trades will be logged but not visualized on chart');
}
console.log(`Plotted ${trades.length} trades with connection lines`);
console.log(`Trade markers created for ${trades.length} positions`);
}
export function clearSimulationMarkers() {
try {
if (window.dashboard && window.dashboard.candleSeries && typeof window.dashboard.candleSeries.setMarkers === 'function') {
window.dashboard.candleSeries.setMarkers([]);
}
} catch (e) {
// Ignore errors clearing markers
}
try {
tradeLineSeries.forEach(series => {
try {
if (window.dashboard && window.dashboard.chart) {
window.dashboard.chart.removeSeries(series);
}
} catch (e) {
// Series might already be removed
}
});
} catch (e) {
// Ignore errors removing series
}
tradeLineSeries = [];
try {
if (htsVisualizer) {
htsVisualizer.clear();
htsVisualizer = null;
}
} catch (e) {
// Ignore errors clearing HTS visualizer
}
// Clear trade marker series
try {
tradeMarkerSeries.forEach(marker => {
try {
if (window.dashboard && window.dashboard.chart && marker.series) {
window.dashboard.chart.removeSeries(marker.series);
}
} catch (e) {
// Ignore errors
}
});
tradeMarkerSeries = [];
} catch (e) {
// Ignore errors
}
}
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
};
}

View File

@ -1,47 +0,0 @@
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);
}
};

View File

@ -1,318 +0,0 @@
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}`);
}
let data = await response.json();
console.log('Strategies loaded:', data);
if (!data.strategies) {
throw new Error('Invalid response format: missing strategies array');
}
data.strategies = data.strategies.filter(s => s.id !== 'hts_trend');
data.strategies.push({
id: 'hts_trend',
name: 'HTS Trend Strategy',
description: 'Higher Timeframe Trend System with Auto HTS, 1H filters, and channel-based entries/exits',
required_indicators: []
});
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;

View File

@ -25,12 +25,6 @@ from pydantic import BaseModel, Field
# Imports for backtest runner
from src.data_collector.database import DatabaseManager
from src.data_collector.indicator_engine import IndicatorEngine, IndicatorConfig
from src.data_collector.brain import Brain
from src.data_collector.backtester import Backtester
# Imports for strategy discovery
import importlib
from src.strategies.base import BaseStrategy
logging.basicConfig(level=logging.INFO)
@ -103,45 +97,6 @@ async def root():
}
@app.get("/api/v1/strategies")
async def list_strategies(response: Response):
"""List all available trading strategies with metadata"""
# Prevent caching
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
# Strategy registry from brain.py
strategy_registry = {
"ma_trend": "src.strategies.ma_strategy.MAStrategy",
}
strategies = []
for strategy_id, class_path in strategy_registry.items():
try:
module_path, class_name = class_path.rsplit('.', 1)
module = importlib.import_module(module_path)
strategy_class = getattr(module, class_name)
# Instantiate to get metadata
strategy_instance = strategy_class()
strategies.append({
"id": strategy_id,
"name": strategy_instance.display_name,
"description": strategy_instance.description,
"required_indicators": strategy_instance.required_indicators
})
except Exception as e:
logger.error(f"Failed to load strategy {strategy_id}: {e}")
return {
"strategies": strategies,
"count": len(strategies)
}
@app.get("/api/v1/candles")
async def get_candles(
symbol: str = Query("BTC", description="Trading pair symbol"),
@ -417,50 +372,6 @@ async def list_backtests(symbol: Optional[str] = None, limit: int = 20):
return [dict(row) for row in rows]
class BacktestRequest(BaseModel):
symbol: str = "BTC"
intervals: list[str] = ["37m"]
start_date: str = "2025-01-01" # ISO date
end_date: Optional[str] = None
async def run_backtest_task(req: BacktestRequest):
"""Background task to run backtest"""
db = DatabaseManager(
host=DB_HOST, port=DB_PORT, database=DB_NAME,
user=DB_USER, password=DB_PASSWORD
)
await db.connect()
try:
# Load configs (hardcoded for now to match main.py)
configs = [
IndicatorConfig("ma44", "sma", 44, req.intervals),
IndicatorConfig("ma125", "sma", 125, req.intervals)
]
engine = IndicatorEngine(db, configs)
brain = Brain(db, engine)
backtester = Backtester(db, engine, brain)
start = datetime.fromisoformat(req.start_date).replace(tzinfo=timezone.utc)
end = datetime.fromisoformat(req.end_date).replace(tzinfo=timezone.utc) if req.end_date else datetime.now(timezone.utc)
await backtester.run(req.symbol, req.intervals, start, end)
except Exception as e:
logger.error(f"Backtest failed: {e}")
finally:
await db.disconnect()
@app.post("/api/v1/backtests")
async def trigger_backtest(req: BacktestRequest, background_tasks: BackgroundTasks):
"""Start a backtest in the background"""
background_tasks.add_task(run_backtest_task, req)
return {"message": "Backtest started", "params": req.dict()}
@app.get("/api/v1/ta")
async def get_technical_analysis(
symbol: str = Query("BTC", description="Trading pair symbol"),

View File

@ -6,7 +6,6 @@ from .backfill import HyperliquidBackfill
from .custom_timeframe_generator import CustomTimeframeGenerator
from .indicator_engine import IndicatorEngine, IndicatorConfig
from .brain import Brain, Decision
from .backtester import Backtester
__all__ = [
'HyperliquidWebSocket',
@ -18,6 +17,5 @@ __all__ = [
'IndicatorEngine',
'IndicatorConfig',
'Brain',
'Decision',
'Backtester'
'Decision'
]

View File

@ -1,391 +0,0 @@
"""
Backtester - Historical replay driver for IndicatorEngine + Brain
Iterates over stored candle data to simulate live trading decisions
"""
import asyncio
import json
import logging
from datetime import datetime, timezone
from typing import Dict, List, Optional, Any
from uuid import uuid4
from .database import DatabaseManager
from .indicator_engine import IndicatorEngine, IndicatorConfig
from .brain import Brain, Decision
from .simulator import Account
from src.strategies.base import SignalType
logger = logging.getLogger(__name__)
class Backtester:
"""
Replays historical candle data through IndicatorEngine and Brain.
Uses Simulator (Account) to track PnL, leverage, and fees.
"""
def __init__(
self,
db: DatabaseManager,
indicator_engine: IndicatorEngine,
brain: Brain,
):
self.db = db
self.indicator_engine = indicator_engine
self.brain = brain
self.account = Account(initial_balance=1000.0)
async def run(
self,
symbol: str,
intervals: List[str],
start: datetime,
end: datetime,
config: Optional[Dict[str, Any]] = None,
) -> str:
"""
Run a full backtest over the given time range.
"""
backtest_id = str(uuid4())
logger.info(
f"Starting backtest {backtest_id}: {symbol} "
f"{intervals} from {start} to {end}"
)
# Reset brain state
self.brain.reset_state()
# Reset account for this run
self.account = Account(initial_balance=1000.0)
# Store the run metadata
await self._save_run_start(
backtest_id, symbol, intervals, start, end, config
)
total_decisions = 0
for interval in intervals:
# Only process intervals that have indicators configured
configured = self.indicator_engine.get_configured_intervals()
if interval not in configured:
logger.warning(
f"Skipping interval {interval}: no indicators configured"
)
continue
# Get all candle timestamps in range
timestamps = await self._get_candle_timestamps(
symbol, interval, start, end
)
if not timestamps:
logger.warning(
f"No candles found for {symbol}/{interval} in range"
)
continue
logger.info(
f"Backtest {backtest_id}: processing {len(timestamps)} "
f"{interval} candles..."
)
for i, ts in enumerate(timestamps):
# 1. Compute indicators
raw_indicators = await self.indicator_engine.compute_at(
symbol, interval, ts
)
indicators = {k: v for k, v in raw_indicators.items() if v is not None}
# 2. Get Current Position info for Strategy
current_pos = self.account.get_position_dict()
# 3. Brain Evaluate
decision: Decision = await self.brain.evaluate(
symbol=symbol,
interval=interval,
timestamp=ts,
indicators=indicators,
backtest_id=backtest_id,
current_position=current_pos
)
# 4. Execute Decision in Simulator
self._execute_decision(decision)
total_decisions += 1
if (i + 1) % 200 == 0:
logger.info(
f"Backtest {backtest_id}: {i + 1}/{len(timestamps)} "
f"{interval} candles processed. Eq: {self.account.equity:.2f}"
)
await asyncio.sleep(0.01)
# Compute and store summary results from Simulator
results = self.account.get_stats()
results['total_evaluations'] = total_decisions
await self._save_run_results(backtest_id, results)
logger.info(
f"Backtest {backtest_id} complete. Final Balance: {results['final_balance']:.2f}"
)
return backtest_id
def _execute_decision(self, decision: Decision):
"""Translate Brain decision into Account action"""
price = decision.price_at_decision
time = decision.time
# Open Long
if decision.decision_type == SignalType.OPEN_LONG.value:
self.account.open_position(time, 'long', price, leverage=1.0) # Todo: Configurable leverage
# Open Short
elif decision.decision_type == SignalType.OPEN_SHORT.value:
self.account.open_position(time, 'short', price, leverage=1.0)
# Close Long (only if we are long)
elif decision.decision_type == SignalType.CLOSE_LONG.value:
if self.account.current_position and self.account.current_position.side == 'long':
self.account.close_position(time, price)
# Close Short (only if we are short)
elif decision.decision_type == SignalType.CLOSE_SHORT.value:
if self.account.current_position and self.account.current_position.side == 'short':
self.account.close_position(time, price)
# Update equity mark-to-market
self.account.update_equity(price)
async def _get_candle_timestamps(
self,
symbol: str,
interval: str,
start: datetime,
end: datetime,
) -> List[datetime]:
"""Get all candle timestamps in a range, ordered chronologically"""
async with self.db.acquire() as conn:
rows = await conn.fetch("""
SELECT time FROM candles
WHERE symbol = $1 AND interval = $2
AND time >= $3 AND time <= $4
ORDER BY time ASC
""", symbol, interval, start, end)
return [row["time"] for row in rows]
async def _save_run_start(
self,
backtest_id: str,
symbol: str,
intervals: List[str],
start: datetime,
end: datetime,
config: Optional[Dict[str, Any]],
) -> None:
"""Store backtest run metadata at start"""
async with self.db.acquire() as conn:
await conn.execute("""
INSERT INTO backtest_runs (
id, strategy, symbol, start_time, end_time,
intervals, config
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
""",
backtest_id,
self.brain.strategy_name,
symbol,
start,
end,
intervals,
json.dumps(config) if config else None,
)
async def _compute_results(self, backtest_id, symbol):
"""Deprecated: Logic moved to Account class"""
return {}
async def _save_run_results(
self,
backtest_id: str,
results: Dict[str, Any],
) -> None:
"""Update backtest run with final results"""
# Remove trades list from stored results (can be large)
stored_results = {k: v for k, v in results.items() if k != "trades"}
async with self.db.acquire() as conn:
await conn.execute("""
UPDATE backtest_runs
SET results = $1
WHERE id = $2
""", json.dumps(stored_results), backtest_id)
async def get_run(self, backtest_id: str) -> Optional[Dict[str, Any]]:
"""Get a specific backtest run with results"""
async with self.db.acquire() as conn:
row = await conn.fetchrow("""
SELECT id, strategy, symbol, start_time, end_time,
intervals, config, results, created_at
FROM backtest_runs
WHERE id = $1
""", backtest_id)
return dict(row) if row else None
async def list_runs(
self,
symbol: Optional[str] = None,
limit: int = 20,
) -> List[Dict[str, Any]]:
"""List recent backtest runs"""
async with self.db.acquire() as conn:
if symbol:
rows = await conn.fetch("""
SELECT id, strategy, symbol, start_time, end_time,
intervals, results, created_at
FROM backtest_runs
WHERE symbol = $1
ORDER BY created_at DESC
LIMIT $2
""", symbol, limit)
else:
rows = await conn.fetch("""
SELECT id, strategy, symbol, start_time, end_time,
intervals, results, created_at
FROM backtest_runs
ORDER BY created_at DESC
LIMIT $1
""", limit)
return [dict(row) for row in rows]
async def cleanup_run(self, backtest_id: str) -> int:
"""Delete all decisions and metadata for a backtest run"""
async with self.db.acquire() as conn:
result = await conn.execute("""
DELETE FROM decisions WHERE backtest_id = $1
""", backtest_id)
await conn.execute("""
DELETE FROM backtest_runs WHERE id = $1
""", backtest_id)
deleted = int(result.split()[-1]) if result else 0
logger.info(
f"Cleaned up backtest {backtest_id}: "
f"{deleted} decisions deleted"
)
return deleted
async def main():
"""CLI entry point for running backtests"""
import argparse
import os
parser = argparse.ArgumentParser(
description="Run backtest on historical data"
)
parser.add_argument(
"--symbol", default="BTC", help="Symbol (default: BTC)"
)
parser.add_argument(
"--intervals", nargs="+", default=["37m"],
help="Intervals to backtest (default: 37m)"
)
parser.add_argument(
"--start", required=True,
help="Start date (ISO format, e.g., 2025-01-01)"
)
parser.add_argument(
"--end", default=None,
help="End date (ISO format, default: now)"
)
parser.add_argument(
"--db-host", default=os.getenv("DB_HOST", "localhost"),
)
parser.add_argument(
"--db-port", type=int, default=int(os.getenv("DB_PORT", 5432)),
)
parser.add_argument(
"--db-name", default=os.getenv("DB_NAME", "btc_data"),
)
parser.add_argument(
"--db-user", default=os.getenv("DB_USER", "btc_bot"),
)
parser.add_argument(
"--db-password", default=os.getenv("DB_PASSWORD", ""),
)
args = parser.parse_args()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# Parse dates
start = datetime.fromisoformat(args.start).replace(tzinfo=timezone.utc)
end = (
datetime.fromisoformat(args.end).replace(tzinfo=timezone.utc)
if args.end
else datetime.now(timezone.utc)
)
# Initialize components
db = DatabaseManager(
host=args.db_host,
port=args.db_port,
database=args.db_name,
user=args.db_user,
password=args.db_password,
)
await db.connect()
try:
# Default indicator configs (MA44 + MA125 on selected intervals)
configs = [
IndicatorConfig("ma44", "sma", 44, args.intervals),
IndicatorConfig("ma125", "sma", 125, args.intervals),
]
indicator_engine = IndicatorEngine(db, configs)
brain = Brain(db, indicator_engine)
backtester = Backtester(db, indicator_engine, brain)
# Run the backtest
backtest_id = await backtester.run(
symbol=args.symbol,
intervals=args.intervals,
start=start,
end=end,
)
# Print results
run = await backtester.get_run(backtest_id)
if run and run.get("results"):
results = json.loads(run["results"]) if isinstance(run["results"], str) else run["results"]
print("\n=== Backtest Results ===")
print(f"ID: {backtest_id}")
print(f"Strategy: {run['strategy']}")
print(f"Period: {run['start_time']} to {run['end_time']}")
print(f"Intervals: {run['intervals']}")
print(f"Total evaluations: {results.get('total_evaluations', 0)}")
print(f"Total trades: {results.get('total_trades', 0)}")
print(f"Win rate: {results.get('win_rate', 0)}%")
print(f"Total P&L: {results.get('total_pnl_pct', 0)}%")
print(f"Final Balance: {results.get('final_balance', 0)}")
finally:
await db.disconnect()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,49 +1,25 @@
"""
Brain - Strategy evaluation and decision logging
Pure strategy logic separated from DB I/O for testability
Brain - Simplified indicator evaluation
"""
import json
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Dict, Optional, Any, List, Callable
from typing import Dict, Optional, Any, List
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__)
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:
"""Load a strategy instance from registry"""
if strategy_name not in STRATEGY_REGISTRY:
logger.warning(f"Strategy {strategy_name} not found, defaulting to ma_trend")
strategy_name = "ma_trend"
factory = STRATEGY_REGISTRY[strategy_name]
return factory()
@dataclass
class Decision:
"""A single brain evaluation result"""
time: datetime
symbol: str
interval: str
decision_type: str # "buy", "sell", "hold" -> Now maps to SignalType
decision_type: str # "buy", "sell", "hold"
strategy: str
confidence: float
price_at_decision: float
@ -71,21 +47,21 @@ class Decision:
class Brain:
"""
Evaluates market conditions using a loaded Strategy.
Evaluates market conditions using indicators.
Simplified version without complex strategy plug-ins.
"""
def __init__(
self,
db: DatabaseManager,
indicator_engine: IndicatorEngine,
strategy: str = "ma44_strategy",
strategy: str = "default",
):
self.db = db
self.indicator_engine = indicator_engine
self.strategy_name = strategy
self.active_strategy: BaseStrategy = load_strategy(strategy)
logger.info(f"Brain initialized with strategy: {self.active_strategy.name}")
logger.info("Brain initialized (Simplified)")
async def evaluate(
self,
@ -120,23 +96,19 @@ class Brain:
"volume": float(candle["volume"]),
}
# Delegate to Strategy
signal: StrategySignal = self.active_strategy.analyze(
candle_dict, indicators, current_position
)
# Build decision
# Simple crossover logic example if needed, otherwise just return HOLD
# For now, we just return a neutral decision as "Strategies" are removed
decision = Decision(
time=timestamp,
symbol=symbol,
interval=interval,
decision_type=signal.type.value,
decision_type="hold",
strategy=self.strategy_name,
confidence=signal.confidence,
confidence=0.0,
price_at_decision=price,
indicator_snapshot=indicators,
candle_snapshot=candle_dict,
reasoning=signal.reasoning,
reasoning="Strategy logic removed - Dashboard shows indicators",
backtest_id=backtest_id,
)
@ -178,8 +150,6 @@ class Brain:
async def _store_decision(self, decision: Decision) -> None:
"""Write decision to the decisions table"""
# Note: We might want to skip writing every single HOLD to DB to save space if simulating millions of candles
# But keeping it for now for full traceability
async with self.db.acquire() as conn:
await conn.execute("""
INSERT INTO decisions (

View File

@ -1,160 +0,0 @@
"""
Simulator
Handles account accounting, leverage, fees, and position management for backtesting.
"""
from dataclasses import dataclass
from typing import Optional, List, Dict, Any
from datetime import datetime
from .brain import Decision # We might need to decouple this later, but reusing for now
@dataclass
class Trade:
entry_time: datetime
exit_time: Optional[datetime]
side: str # 'long' or 'short'
entry_price: float
exit_price: Optional[float]
size: float # Quantity of asset
leverage: float
pnl: float = 0.0
pnl_percent: float = 0.0
fees: float = 0.0
status: str = 'open' # 'open', 'closed'
class Account:
def __init__(self, initial_balance: float = 1000.0, maker_fee: float = 0.0002, taker_fee: float = 0.0005):
self.initial_balance = initial_balance
self.balance = initial_balance
self.equity = initial_balance
self.maker_fee = maker_fee
self.taker_fee = taker_fee
self.trades: List[Trade] = []
self.current_position: Optional[Trade] = None
self.margin_used = 0.0
def update_equity(self, current_price: float):
"""Update equity based on unrealized PnL of current position"""
if not self.current_position:
self.equity = self.balance
return
trade = self.current_position
if trade.side == 'long':
unrealized_pnl = (current_price - trade.entry_price) * trade.size
else:
unrealized_pnl = (trade.entry_price - current_price) * trade.size
self.equity = self.balance + unrealized_pnl
def open_position(self, time: datetime, side: str, price: float, leverage: float = 1.0, portion: float = 1.0):
"""
Open a position.
portion: 0.0 to 1.0 (percentage of available balance to use)
"""
if self.current_position:
# Already have a position, ignore for now (or could add to it)
return
# Calculate position size
# Margin = (Balance * portion)
# Position Value = Margin * Leverage
# Size = Position Value / Price
margin_to_use = self.balance * portion
position_value = margin_to_use * leverage
size = position_value / price
# Fee (Taker)
fee = position_value * self.taker_fee
self.balance -= fee # Deduct fee immediately
self.current_position = Trade(
entry_time=time,
exit_time=None,
side=side,
entry_price=price,
exit_price=None,
size=size,
leverage=leverage,
fees=fee
)
self.margin_used = margin_to_use
def close_position(self, time: datetime, price: float):
"""Close the current position"""
if not self.current_position:
return
trade = self.current_position
position_value = trade.size * price
# Calculate PnL
if trade.side == 'long':
pnl = (price - trade.entry_price) * trade.size
pnl_pct = (price - trade.entry_price) / trade.entry_price * trade.leverage * 100
else:
pnl = (trade.entry_price - price) * trade.size
pnl_pct = (trade.entry_price - price) / trade.entry_price * trade.leverage * 100
# Fee (Taker)
fee = position_value * self.taker_fee
self.balance -= fee
trade.fees += fee
# Update Balance
self.balance += pnl
self.margin_used = 0.0
# Update Trade Record
trade.exit_time = time
trade.exit_price = price
trade.pnl = pnl
trade.pnl_percent = pnl_pct
trade.status = 'closed'
self.trades.append(trade)
self.current_position = None
self.equity = self.balance
def get_position_dict(self) -> Optional[Dict[str, Any]]:
if not self.current_position:
return None
return {
'type': self.current_position.side,
'entry_price': self.current_position.entry_price,
'size': self.current_position.size,
'leverage': self.current_position.leverage
}
def get_stats(self) -> Dict[str, Any]:
wins = [t for t in self.trades if t.pnl > 0]
losses = [t for t in self.trades if t.pnl <= 0]
total_pnl = self.balance - self.initial_balance
total_pnl_pct = (total_pnl / self.initial_balance) * 100
return {
"initial_balance": self.initial_balance,
"final_balance": self.balance,
"total_pnl": total_pnl,
"total_pnl_pct": total_pnl_pct,
"total_trades": len(self.trades),
"win_count": len(wins),
"loss_count": len(losses),
"win_rate": (len(wins) / len(self.trades) * 100) if self.trades else 0.0,
"max_drawdown": 0.0, # Todo: implement DD tracking
"trades": [
{
"entry_time": t.entry_time.isoformat(),
"exit_time": t.exit_time.isoformat() if t.exit_time else None,
"side": t.side,
"entry_price": t.entry_price,
"exit_price": t.exit_price,
"pnl": t.pnl,
"pnl_pct": t.pnl_percent,
"fees": t.fees
}
for t in self.trades
]
}

View File

@ -1,68 +0,0 @@
"""
Base Strategy Interface
All strategies must inherit from this class.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Dict, Any, List, Optional
from enum import Enum
class SignalType(Enum):
OPEN_LONG = "open_long"
OPEN_SHORT = "open_short"
CLOSE_LONG = "close_long"
CLOSE_SHORT = "close_short"
HOLD = "hold"
@dataclass
class StrategySignal:
type: SignalType
confidence: float
reasoning: str
class BaseStrategy(ABC):
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config or {}
@property
@abstractmethod
def name(self) -> str:
"""Unique identifier for the strategy"""
pass
@property
@abstractmethod
def required_indicators(self) -> List[str]:
"""List of indicator names required by this strategy (e.g. ['ma44'])"""
pass
@property
@abstractmethod
def display_name(self) -> str:
"""User-friendly name for display in UI (e.g. 'MA44 Crossover')"""
pass
@property
@abstractmethod
def description(self) -> str:
"""Detailed description of how the strategy works"""
pass
@abstractmethod
def analyze(
self,
candle: Dict[str, Any],
indicators: Dict[str, float],
current_position: Optional[Dict[str, Any]] = None
) -> StrategySignal:
"""
Analyze market data and return a trading signal.
Args:
candle: Dictionary containing 'close', 'open', 'high', 'low', 'volume', 'time'
indicators: Dictionary of pre-computed indicator values
current_position: Details about current open position (if any)
{'type': 'long'/'short', 'entry_price': float, 'size': float}
"""
pass

View File

@ -1,77 +0,0 @@
"""
Moving Average Strategy
Configurable trend following strategy.
- Long when Price > MA(period)
- Short when Price < MA(period)
"""
from typing import Dict, Any, List, Optional
from .base import BaseStrategy, StrategySignal, SignalType
class MAStrategy(BaseStrategy):
"""
Configurable Moving Average Strategy.
Config:
- period: int - MA period (default: 44)
"""
DEFAULT_PERIOD = 44
@property
def name(self) -> str:
return "ma_trend"
@property
def required_indicators(self) -> List[str]:
# Dynamic based on config
period = self.config.get('period', self.DEFAULT_PERIOD)
return [f"ma{period}"]
@property
def display_name(self) -> str:
return "MA Strategy"
@property
def description(self) -> str:
return "Configurable Moving Average strategy. Parameters: period (5-500, default: 44). Goes long when price > MA(period), short when price < MA(period). Optional multi-timeframe trend filter available."
def analyze(
self,
candle: Dict[str, Any],
indicators: Dict[str, float],
current_position: Optional[Dict[str, Any]] = None
) -> StrategySignal:
period = self.config.get('period', self.DEFAULT_PERIOD)
ma_key = f"ma{period}"
price = candle['close']
ma_value = indicators.get(ma_key)
if ma_value is None:
return StrategySignal(SignalType.HOLD, 0.0, f"MA{period} not available")
# Current position state
is_long = current_position and current_position.get('type') == 'long'
is_short = current_position and current_position.get('type') == 'short'
# Logic: Price > MA -> Bullish
if price > ma_value:
if is_long:
return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} > MA{period} {ma_value:.2f}. Stay Long.")
elif is_short:
return StrategySignal(SignalType.CLOSE_SHORT, 1.0, f"Price {price:.2f} crossed above MA{period} {ma_value:.2f}. Close Short.")
else:
return StrategySignal(SignalType.OPEN_LONG, 1.0, f"Price {price:.2f} > MA{period} {ma_value:.2f}. Open Long.")
# Logic: Price < MA -> Bearish
elif price < ma_value:
if is_short:
return StrategySignal(SignalType.HOLD, 1.0, f"Price {price:.2f} < MA{period} {ma_value:.2f}. Stay Short.")
elif is_long:
return StrategySignal(SignalType.CLOSE_LONG, 1.0, f"Price {price:.2f} crossed below MA{period} {ma_value:.2f}. Close Long.")
else:
return StrategySignal(SignalType.OPEN_SHORT, 1.0, f"Price {price:.2f} < MA{period} {ma_value:.2f}. Open Short.")
return StrategySignal(SignalType.HOLD, 0.0, f"Price == MA{period}")