Pre-refactor: commit before converting indicators to self-contained files
This commit is contained in:
@ -1392,7 +1392,6 @@
|
|||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="sidebar-tabs">
|
<div class="sidebar-tabs">
|
||||||
<button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button>
|
<button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button>
|
||||||
<button class="sidebar-tab" data-tab="strategies">📋 Strategies</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-toggle" id="sidebarToggleBtn">◀</button>
|
<button class="sidebar-toggle" id="sidebarToggleBtn">◀</button>
|
||||||
</div>
|
</div>
|
||||||
@ -1406,113 +1405,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,25 +1,5 @@
|
|||||||
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
|
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
|
||||||
import { restoreSidebarState, toggleSidebar, initSidebarTabs, restoreSidebarTabState } from './ui/sidebar.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 {
|
import {
|
||||||
initIndicatorPanel,
|
initIndicatorPanel,
|
||||||
getActiveIndicators,
|
getActiveIndicators,
|
||||||
@ -28,42 +8,13 @@ import {
|
|||||||
addIndicator,
|
addIndicator,
|
||||||
removeIndicatorById
|
removeIndicatorById
|
||||||
} from './ui/indicators-panel-new.js';
|
} from './ui/indicators-panel-new.js';
|
||||||
import { StrategyParams } from './strategies/config.js';
|
|
||||||
import { IndicatorRegistry } from './indicators/index.js';
|
import { IndicatorRegistry } from './indicators/index.js';
|
||||||
|
|
||||||
window.dashboard = null;
|
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.toggleSidebar = toggleSidebar;
|
||||||
window.refreshTA = refreshTA;
|
window.refreshTA = refreshTA;
|
||||||
window.openAIAnalysis = openAIAnalysis;
|
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() {
|
window.renderIndicatorList = function() {
|
||||||
// This function is no longer needed for sidebar indicators
|
// This function is no longer needed for sidebar indicators
|
||||||
};
|
};
|
||||||
@ -73,8 +24,6 @@ window.initIndicatorPanel = initIndicatorPanel;
|
|||||||
window.addIndicator = addIndicator;
|
window.addIndicator = addIndicator;
|
||||||
window.toggleIndicator = addIndicator;
|
window.toggleIndicator = addIndicator;
|
||||||
|
|
||||||
window.StrategyParams = StrategyParams;
|
|
||||||
window.SimulationStorage = SimulationStorage;
|
|
||||||
window.IndicatorRegistry = IndicatorRegistry;
|
window.IndicatorRegistry = IndicatorRegistry;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
@ -88,11 +37,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
restoreSidebarState();
|
restoreSidebarState();
|
||||||
restoreSidebarTabState();
|
restoreSidebarTabState();
|
||||||
initSidebarTabs();
|
initSidebarTabs();
|
||||||
setDefaultStartDate();
|
|
||||||
updateTimeframeDisplay();
|
|
||||||
renderSavedSimulations();
|
|
||||||
|
|
||||||
await loadStrategies();
|
|
||||||
|
|
||||||
// Initialize indicator panel
|
// Initialize indicator panel
|
||||||
window.initIndicatorPanel();
|
window.initIndicatorPanel();
|
||||||
|
|||||||
@ -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 }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export { StrategyParams } from './config.js';
|
|
||||||
export { RiskManager } from './risk-manager.js';
|
|
||||||
export { ClientStrategyEngine } from './engine.js';
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import { HTSStrategyEngine } from '../strategies/hts-engine.js';
|
|
||||||
|
|
||||||
const HTS_COLORS = {
|
const HTS_COLORS = {
|
||||||
fastHigh: '#00bcd4',
|
fastHigh: '#00bcd4',
|
||||||
fastLow: '#00bcd4',
|
fastLow: '#00bcd4',
|
||||||
|
|||||||
@ -1,28 +1,5 @@
|
|||||||
export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js';
|
export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js';
|
||||||
export { toggleSidebar, restoreSidebarState } from './sidebar.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 {
|
export {
|
||||||
renderIndicatorList,
|
renderIndicatorList,
|
||||||
addNewIndicator,
|
addNewIndicator,
|
||||||
|
|||||||
@ -298,6 +298,47 @@ function renderIndicatorConfig(indicator, meta) {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</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 ? `
|
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<div class="section-subtitle">Parameters</div>
|
<div class="section-subtitle">Parameters</div>
|
||||||
|
|||||||
@ -236,18 +236,16 @@ function calculateMASignal(indicator, lastCandle, prevCandle, values) {
|
|||||||
|
|
||||||
let signalType, strength, reasoning;
|
let signalType, strength, reasoning;
|
||||||
|
|
||||||
if (close > ma * 1.02) {
|
if (close > ma) {
|
||||||
signalType = SIGNAL_TYPES.BUY;
|
signalType = SIGNAL_TYPES.BUY;
|
||||||
strength = Math.min(60 + ((close - ma) / ma) * 500, 100);
|
strength = Math.min(60 + ((close - ma) / ma) * 500, 100);
|
||||||
reasoning = `Price (${close.toFixed(2)}) is strongly above ${maLabel} (${ma.toFixed(2)}), bullish trend`;
|
reasoning = `Price (${close.toFixed(2)}) is above ${maLabel} (${ma.toFixed(2)})`;
|
||||||
} else if (close < ma * 0.98) {
|
} else if (close < ma) {
|
||||||
signalType = SIGNAL_TYPES.SELL;
|
signalType = SIGNAL_TYPES.SELL;
|
||||||
strength = Math.min(60 + ((ma - close) / ma) * 500, 100);
|
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 {
|
} else {
|
||||||
signalType = SIGNAL_TYPES.HOLD;
|
return null;
|
||||||
strength = 30;
|
|
||||||
reasoning = `Price (${close.toFixed(2)}) is near ${maLabel} (${ma.toFixed(2)}), sideways/consolidating`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[calculateMASignal] Result:', signalType, strength);
|
console.log('[calculateMASignal] Result:', signalType, strength);
|
||||||
@ -406,6 +404,36 @@ export function calculateAllIndicatorSignals() {
|
|||||||
|
|
||||||
const signal = calculateIndicatorSignal(indicator, candles, values);
|
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 label = indicator.type?.toUpperCase();
|
||||||
const params = indicator.params && typeof indicator.params === 'object'
|
const params = indicator.params && typeof indicator.params === 'object'
|
||||||
? Object.entries(indicator.params)
|
? Object.entries(indicator.params)
|
||||||
@ -420,11 +448,12 @@ export function calculateAllIndicatorSignals() {
|
|||||||
label: label,
|
label: label,
|
||||||
params: params || null,
|
params: params || null,
|
||||||
type: indicator.type,
|
type: indicator.type,
|
||||||
signal: signal.type,
|
signal: currentSignal.type,
|
||||||
strength: Math.round(signal.strength),
|
strength: Math.round(currentSignal.strength),
|
||||||
value: signal.value,
|
value: currentSignal.value,
|
||||||
reasoning: signal.reasoning,
|
reasoning: currentSignal.reasoning,
|
||||||
color: SIGNAL_COLORS[signal.type]
|
color: SIGNAL_COLORS[currentSignal.type],
|
||||||
|
lastSignalDate: lastSignalDate
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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;
|
|
||||||
@ -25,12 +25,6 @@ from pydantic import BaseModel, Field
|
|||||||
# Imports for backtest runner
|
# Imports for backtest runner
|
||||||
from src.data_collector.database import DatabaseManager
|
from src.data_collector.database import DatabaseManager
|
||||||
from src.data_collector.indicator_engine import IndicatorEngine, IndicatorConfig
|
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)
|
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")
|
@app.get("/api/v1/candles")
|
||||||
async def get_candles(
|
async def get_candles(
|
||||||
symbol: str = Query("BTC", description="Trading pair symbol"),
|
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]
|
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")
|
@app.get("/api/v1/ta")
|
||||||
async def get_technical_analysis(
|
async def get_technical_analysis(
|
||||||
symbol: str = Query("BTC", description="Trading pair symbol"),
|
symbol: str = Query("BTC", description="Trading pair symbol"),
|
||||||
|
|||||||
@ -6,7 +6,6 @@ from .backfill import HyperliquidBackfill
|
|||||||
from .custom_timeframe_generator import CustomTimeframeGenerator
|
from .custom_timeframe_generator import CustomTimeframeGenerator
|
||||||
from .indicator_engine import IndicatorEngine, IndicatorConfig
|
from .indicator_engine import IndicatorEngine, IndicatorConfig
|
||||||
from .brain import Brain, Decision
|
from .brain import Brain, Decision
|
||||||
from .backtester import Backtester
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'HyperliquidWebSocket',
|
'HyperliquidWebSocket',
|
||||||
@ -18,6 +17,5 @@ __all__ = [
|
|||||||
'IndicatorEngine',
|
'IndicatorEngine',
|
||||||
'IndicatorConfig',
|
'IndicatorConfig',
|
||||||
'Brain',
|
'Brain',
|
||||||
'Decision',
|
'Decision'
|
||||||
'Backtester'
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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())
|
|
||||||
@ -1,49 +1,25 @@
|
|||||||
"""
|
"""
|
||||||
Brain - Strategy evaluation and decision logging
|
Brain - Simplified indicator evaluation
|
||||||
Pure strategy logic separated from DB I/O for testability
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
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 .database import DatabaseManager
|
||||||
from .indicator_engine import IndicatorEngine
|
from .indicator_engine import IndicatorEngine
|
||||||
from src.strategies.base import BaseStrategy, StrategySignal, SignalType
|
|
||||||
from src.strategies.ma_strategy import MAStrategy
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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
|
@dataclass
|
||||||
class Decision:
|
class Decision:
|
||||||
"""A single brain evaluation result"""
|
"""A single brain evaluation result"""
|
||||||
time: datetime
|
time: datetime
|
||||||
symbol: str
|
symbol: str
|
||||||
interval: str
|
interval: str
|
||||||
decision_type: str # "buy", "sell", "hold" -> Now maps to SignalType
|
decision_type: str # "buy", "sell", "hold"
|
||||||
strategy: str
|
strategy: str
|
||||||
confidence: float
|
confidence: float
|
||||||
price_at_decision: float
|
price_at_decision: float
|
||||||
@ -71,21 +47,21 @@ class Decision:
|
|||||||
|
|
||||||
class Brain:
|
class Brain:
|
||||||
"""
|
"""
|
||||||
Evaluates market conditions using a loaded Strategy.
|
Evaluates market conditions using indicators.
|
||||||
|
Simplified version without complex strategy plug-ins.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
db: DatabaseManager,
|
db: DatabaseManager,
|
||||||
indicator_engine: IndicatorEngine,
|
indicator_engine: IndicatorEngine,
|
||||||
strategy: str = "ma44_strategy",
|
strategy: str = "default",
|
||||||
):
|
):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.indicator_engine = indicator_engine
|
self.indicator_engine = indicator_engine
|
||||||
self.strategy_name = strategy
|
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(
|
async def evaluate(
|
||||||
self,
|
self,
|
||||||
@ -120,23 +96,19 @@ class Brain:
|
|||||||
"volume": float(candle["volume"]),
|
"volume": float(candle["volume"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Delegate to Strategy
|
# Simple crossover logic example if needed, otherwise just return HOLD
|
||||||
signal: StrategySignal = self.active_strategy.analyze(
|
# For now, we just return a neutral decision as "Strategies" are removed
|
||||||
candle_dict, indicators, current_position
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build decision
|
|
||||||
decision = Decision(
|
decision = Decision(
|
||||||
time=timestamp,
|
time=timestamp,
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
decision_type=signal.type.value,
|
decision_type="hold",
|
||||||
strategy=self.strategy_name,
|
strategy=self.strategy_name,
|
||||||
confidence=signal.confidence,
|
confidence=0.0,
|
||||||
price_at_decision=price,
|
price_at_decision=price,
|
||||||
indicator_snapshot=indicators,
|
indicator_snapshot=indicators,
|
||||||
candle_snapshot=candle_dict,
|
candle_snapshot=candle_dict,
|
||||||
reasoning=signal.reasoning,
|
reasoning="Strategy logic removed - Dashboard shows indicators",
|
||||||
backtest_id=backtest_id,
|
backtest_id=backtest_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -178,8 +150,6 @@ class Brain:
|
|||||||
|
|
||||||
async def _store_decision(self, decision: Decision) -> None:
|
async def _store_decision(self, decision: Decision) -> None:
|
||||||
"""Write decision to the decisions table"""
|
"""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:
|
async with self.db.acquire() as conn:
|
||||||
await conn.execute("""
|
await conn.execute("""
|
||||||
INSERT INTO decisions (
|
INSERT INTO decisions (
|
||||||
|
|||||||
@ -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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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}")
|
|
||||||
Reference in New Issue
Block a user