feat: add 5 dashboard features — dark mode, trade history, backtests, model insights, alerts
- Dark mode: class-based theme toggle with localStorage persistence and flash prevention - Trade History (/trades): paginated table, stats cards, equity curve chart with DB API endpoints - Backtest Viewer (/backtests): log parser for 35 backtest results, sidebar + detail + comparison tabs - Model Insights: dashboard card + dialog showing feature importance, regime distribution, training history - Alert/Signal Log (/alerts): signal stats, filterable table with execution tracking - API: 8 new endpoints with psycopg2 DB connection pool - Dark mode sweep across books page, about dialog, and all dashboard components - Architecture docs rewritten with Mermaid diagrams (23 docs) - README and FEATURES.md rewritten bilingual (Indonesian + English) - main_live.py: write model_metrics.json on startup and retrain Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Script to generate src/data/backtests.ts from backtest result log files.
|
||||
* Scans backtests/*_results/ for the most recent .log file in each directory,
|
||||
* parses performance metrics and trade log, and outputs static TS data.
|
||||
*
|
||||
* Run: node scripts/generate-backtests.js
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const BACKTESTS_DIR = path.resolve(__dirname, "..", "..", "backtests");
|
||||
const OUT = path.join(__dirname, "..", "src", "data", "backtests.ts");
|
||||
|
||||
function escapeForTemplate(str) {
|
||||
return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
||||
}
|
||||
|
||||
function parseMetric(text, pattern) {
|
||||
const m = text.match(pattern);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
function parseNumber(text, pattern) {
|
||||
const val = parseMetric(text, pattern);
|
||||
if (!val) return 0;
|
||||
return parseFloat(val.replace(/[,$]/g, "")) || 0;
|
||||
}
|
||||
|
||||
function parsePercent(text, pattern) {
|
||||
const val = parseMetric(text, pattern);
|
||||
if (!val) return 0;
|
||||
return parseFloat(val.replace("%", "")) || 0;
|
||||
}
|
||||
|
||||
function parseExitReasons(text) {
|
||||
const section = text.match(/--- EXIT REASON(?:S| BREAKDOWN) ---\n([\s\S]*?)(?=\n---|\n\n\n)/);
|
||||
if (!section) return [];
|
||||
const lines = section[1].trim().split("\n");
|
||||
return lines.map((line) => {
|
||||
const m = line.match(/^\s*(\S+)\s*:\s*(\d+)\s*\(\s*([\d.]+)%\)/);
|
||||
if (!m) return null;
|
||||
return { reason: m[1], count: parseInt(m[2]), pct: parseFloat(m[3]) };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function parseDirectionBreakdown(text) {
|
||||
const section = text.match(/--- DIRECTION(?:\s+BREAKDOWN)? ---\n([\s\S]*?)(?=\n---|\n\n\n)/);
|
||||
if (!section) return [];
|
||||
const lines = section[1].trim().split("\n");
|
||||
return lines.map((line) => {
|
||||
const m = line.match(/^\s*(BUY|SELL):\s*(\d+)\s*trades?,\s*([\d.]+)%\s*WR,\s*\$\s*([-\d,.]+)/);
|
||||
if (!m) return null;
|
||||
return { direction: m[1], trades: parseInt(m[2]), winRate: parseFloat(m[3]), pnl: parseFloat(m[4].replace(/,/g, "")) };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function parseSessionBreakdown(text) {
|
||||
const section = text.match(/--- SESSION BREAKDOWN ---\n([\s\S]*?)(?=\n---|\n\n\n)/);
|
||||
if (!section) return [];
|
||||
const lines = section[1].trim().split("\n");
|
||||
return lines.map((line) => {
|
||||
const m = line.match(/^\s*(.+?)\s*:\s*(\d+)\s*trades?,\s*([\d.]+)%\s*WR,\s*\$\s*([-\d,.]+)/);
|
||||
if (!m) return null;
|
||||
return { session: m[1].trim(), trades: parseInt(m[2]), winRate: parseFloat(m[3]), pnl: parseFloat(m[4].replace(/,/g, "")) };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function parseTrades(text) {
|
||||
const section = text.match(/--- TRADE LOG ---\n.*\n-+\n([\s\S]*?)$/);
|
||||
if (!section) return [];
|
||||
const lines = section[1].trim().split("\n");
|
||||
return lines.slice(0, 500).map((line) => {
|
||||
// Format: # date time DIR entry exit P/L result exit_reason conf mode session
|
||||
const m = line.match(
|
||||
/^\s*(\d+)\s+(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s+(BUY|SELL)\s+([\d.]+)\s+([\d.]+)\s+([-\d.]+)\s+(WIN|LOSS)\s+(\S+)\s+(\d+)%\s+(\S+)\s+(.+)$/
|
||||
);
|
||||
if (!m) return null;
|
||||
return {
|
||||
num: parseInt(m[1]),
|
||||
time: m[2],
|
||||
dir: m[3],
|
||||
entry: parseFloat(m[4]),
|
||||
exit: parseFloat(m[5]),
|
||||
pnl: parseFloat(m[6]),
|
||||
result: m[7],
|
||||
exitReason: m[8],
|
||||
conf: parseInt(m[9]),
|
||||
mode: m[10],
|
||||
session: m[11].trim(),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function formatName(dirName) {
|
||||
// "01_smc_only_results" -> "SMC Only"
|
||||
return dirName
|
||||
.replace(/_results$/, "")
|
||||
.replace(/^\d+_/, "")
|
||||
.split("_")
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
// Find all backtest result directories
|
||||
const resultDirs = fs
|
||||
.readdirSync(BACKTESTS_DIR)
|
||||
.filter((d) => d.endsWith("_results") && fs.statSync(path.join(BACKTESTS_DIR, d)).isDirectory())
|
||||
.sort();
|
||||
|
||||
console.log(`Found ${resultDirs.length} backtest result directories`);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const dir of resultDirs) {
|
||||
const dirPath = path.join(BACKTESTS_DIR, dir);
|
||||
const logFiles = fs
|
||||
.readdirSync(dirPath)
|
||||
.filter((f) => f.endsWith(".log"))
|
||||
.sort()
|
||||
.reverse(); // most recent first
|
||||
|
||||
if (logFiles.length === 0) {
|
||||
console.warn(` SKIP ${dir}: no .log files`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const logFile = logFiles[0];
|
||||
const logPath = path.join(dirPath, logFile);
|
||||
const text = fs.readFileSync(logPath, "utf-8");
|
||||
|
||||
const idMatch = dir.match(/^(\d+)/);
|
||||
const id = idMatch ? parseInt(idMatch[1]) : results.length + 1;
|
||||
|
||||
const result = {
|
||||
id,
|
||||
slug: dir.replace(/_results$/, ""),
|
||||
name: formatName(dir),
|
||||
logFile,
|
||||
generatedAt: parseMetric(text, /Generated:\s*(.+)/),
|
||||
period: parseMetric(text, /Period:\s*(.+)/),
|
||||
strategy: parseMetric(text, /Strategy:\s*(.+)/),
|
||||
// Support both verbose "Total Trades: 686" and compact "Trades: 683 | WR: 73.4%" formats
|
||||
totalTrades: parseNumber(text, /Total Trades:\s*([\d,]+)/) || parseNumber(text, /Trades:\s*([\d,]+)/),
|
||||
wins: parseNumber(text, /Wins:\s*([\d,]+)/),
|
||||
losses: parseNumber(text, /Losses:\s*([\d,]+)/),
|
||||
winRate: parsePercent(text, /Win Rate:\s*([\d.]+)%/) || parsePercent(text, /WR:\s*([\d.]+)%/),
|
||||
totalProfit: parseNumber(text, /Total Profit:\s*\$([\d,.]+)/),
|
||||
totalLoss: parseNumber(text, /Total Loss:\s*\$([\d,.]+)/),
|
||||
netPnl: parseNumber(text, /Net PnL:\s*\$\s*([-\d,.]+)/),
|
||||
profitFactor: parseNumber(text, /Profit Factor:\s*([\d.]+)/) || parseNumber(text, /PF:\s*([\d.]+)/),
|
||||
maxDrawdown: parsePercent(text, /Max Drawdown:\s*([\d.]+)%/) || parsePercent(text, /Max DD:\s*([\d.]+)%/),
|
||||
maxDrawdownUsd: parseNumber(text, /Max Drawdown:\s*[\d.]+%\s*\(\$([\d,.]+)\)/),
|
||||
avgWin: parseNumber(text, /Avg Win:\s*\$([\d,.]+)/),
|
||||
avgLoss: parseNumber(text, /Avg Loss:\s*\$([\d,.]+)/),
|
||||
expectancy: parseNumber(text, /Expectancy:\s*\$([-\d,.]+)/),
|
||||
sharpeRatio: parseNumber(text, /Sharpe Ratio:\s*([-\d.]+)/) || parseNumber(text, /Sharpe:\s*([-\d.]+)/),
|
||||
exitReasons: parseExitReasons(text),
|
||||
directionBreakdown: parseDirectionBreakdown(text),
|
||||
sessionBreakdown: parseSessionBreakdown(text),
|
||||
tradeCount: 0, // set below
|
||||
};
|
||||
|
||||
// Parse trades (store just count for the static file - trades are big)
|
||||
const trades = parseTrades(text);
|
||||
result.tradeCount = trades.length;
|
||||
|
||||
// Fix negative Net PnL (the regex may miss the sign)
|
||||
if (text.includes("Net PnL:") && text.match(/Net PnL:\s*-/)) {
|
||||
result.netPnl = -Math.abs(result.netPnl);
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
console.log(` OK ${dir}: ${result.totalTrades} trades, ${result.winRate}% WR, $${result.netPnl} PnL`);
|
||||
}
|
||||
|
||||
// Sort by id
|
||||
results.sort((a, b) => a.id - b.id);
|
||||
|
||||
// Generate output
|
||||
const outDir = path.dirname(OUT);
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
let output = `// AUTO-GENERATED — do not edit manually.
|
||||
// Run: node scripts/generate-backtests.js
|
||||
|
||||
export interface ExitReason {
|
||||
reason: string;
|
||||
count: number;
|
||||
pct: number;
|
||||
}
|
||||
|
||||
export interface DirectionBreakdown {
|
||||
direction: string;
|
||||
trades: number;
|
||||
winRate: number;
|
||||
pnl: number;
|
||||
}
|
||||
|
||||
export interface SessionBreakdown {
|
||||
session: string;
|
||||
trades: number;
|
||||
winRate: number;
|
||||
pnl: number;
|
||||
}
|
||||
|
||||
export interface BacktestResult {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
logFile: string;
|
||||
generatedAt: string | null;
|
||||
period: string | null;
|
||||
strategy: string | null;
|
||||
totalTrades: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
winRate: number;
|
||||
totalProfit: number;
|
||||
totalLoss: number;
|
||||
netPnl: number;
|
||||
profitFactor: number;
|
||||
maxDrawdown: number;
|
||||
maxDrawdownUsd: number;
|
||||
avgWin: number;
|
||||
avgLoss: number;
|
||||
expectancy: number;
|
||||
sharpeRatio: number;
|
||||
exitReasons: ExitReason[];
|
||||
directionBreakdown: DirectionBreakdown[];
|
||||
sessionBreakdown: SessionBreakdown[];
|
||||
tradeCount: number;
|
||||
}
|
||||
|
||||
export const backtestResults: BacktestResult[] = ${JSON.stringify(results, null, 2)};
|
||||
`;
|
||||
|
||||
fs.writeFileSync(OUT, output, "utf-8");
|
||||
console.log(`\nGenerated ${OUT} with ${results.length} backtest results.`);
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Script to generate src/data/books.ts from documentation files.
|
||||
* Run: node scripts/generate-books.js
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const OUT = path.join(__dirname, "..", "src", "data", "books.ts");
|
||||
|
||||
// Define all books with their source files and metadata (Indonesian)
|
||||
const bookDefs = [
|
||||
// Mulai di Sini
|
||||
{ slug: "readme", title: "README", category: "Mulai di Sini", icon: "BookOpen", description: "Gambaran proyek, instalasi, dan panduan cepat memulai XAUBot AI", file: path.join(ROOT, "README.md") },
|
||||
{ slug: "features", title: "Fitur & Komponen", category: "Mulai di Sini", icon: "Sparkles", description: "Daftar lengkap fitur — 14 filter entry, 12 kondisi exit, manajemen risiko", file: path.join(ROOT, "docs", "FEATURES.md") },
|
||||
{ slug: "architecture-full", title: "Arsitektur Lengkap", category: "Mulai di Sini", icon: "LayoutDashboard", description: "Arsitektur menyeluruh sistem — alur data, komponen, dan interaksi antar modul", file: path.join(ROOT, "docs", "arsitektur-ai", "00-ARSITEKTUR-LENGKAP.md") },
|
||||
{ slug: "architecture-index", title: "Indeks Arsitektur", category: "Mulai di Sini", icon: "List", description: "Daftar semua dokumen arsitektur dan status komponen terkini", file: path.join(ROOT, "docs", "arsitektur-ai", "README.md") },
|
||||
|
||||
// AI & Analisis
|
||||
{ slug: "hmm-regime", title: "HMM Regime Detector", category: "AI & Analisis", icon: "Brain", description: "Deteksi kondisi pasar menggunakan Hidden Markov Model 3 state", file: path.join(ROOT, "docs", "arsitektur-ai", "01-HMM-Regime-Detector.md") },
|
||||
{ slug: "xgboost", title: "XGBoost Signal Predictor", category: "AI & Analisis", icon: "Cpu", description: "Model machine learning untuk prediksi sinyal BUY/SELL/HOLD", file: path.join(ROOT, "docs", "arsitektur-ai", "02-XGBoost-Signal-Predictor.md") },
|
||||
{ slug: "smc", title: "SMC Analyzer", category: "AI & Analisis", icon: "TrendingUp", description: "Analisis Smart Money Concepts — Order Block, FVG, BOS, CHoCH", file: path.join(ROOT, "docs", "arsitektur-ai", "03-SMC-Analyzer.md") },
|
||||
{ slug: "feature-eng", title: "Feature Engineering", category: "AI & Analisis", icon: "Layers", description: "37 fitur teknikal — RSI, ATR, MACD, Bollinger, dan lainnya", file: path.join(ROOT, "docs", "arsitektur-ai", "04-Feature-Engineering.md") },
|
||||
|
||||
// Risiko & Proteksi
|
||||
{ slug: "risk-management", title: "Manajemen Risiko", category: "Risiko & Proteksi", icon: "Shield", description: "Sistem manajemen risiko dinamis dengan mode kapital dan batas harian", file: path.join(ROOT, "docs", "arsitektur-ai", "05-Risk-Management.md") },
|
||||
{ slug: "session-filter", title: "Filter Sesi", category: "Risiko & Proteksi", icon: "Clock", description: "Filter sesi perdagangan — Sydney, London, New York dalam zona waktu WIB", file: path.join(ROOT, "docs", "arsitektur-ai", "06-Session-Filter.md") },
|
||||
{ slug: "stop-loss", title: "Stop Loss", category: "Risiko & Proteksi", icon: "ShieldAlert", description: "Proteksi SL berbasis ATR dan broker-level untuk keamanan maksimal", file: path.join(ROOT, "docs", "arsitektur-ai", "07-Stop-Loss.md") },
|
||||
{ slug: "take-profit", title: "Take Profit", category: "Risiko & Proteksi", icon: "Target", description: "Target TP multi-level dengan perhitungan ATR dan struktur pasar", file: path.join(ROOT, "docs", "arsitektur-ai", "08-Take-Profit.md") },
|
||||
|
||||
// Proses Trading
|
||||
{ slug: "entry-trade", title: "Entry Trade", category: "Proses Trading", icon: "ArrowRightCircle", description: "14 filter entry dan logika eksekusi perdagangan — dari sinyal hingga order", file: path.join(ROOT, "docs", "arsitektur-ai", "09-Entry-Trade.md") },
|
||||
{ slug: "exit-trade", title: "Exit Trade", category: "Proses Trading", icon: "ArrowLeftCircle", description: "12 kondisi exit termasuk trailing SL, batas waktu, dan perubahan regime", file: path.join(ROOT, "docs", "arsitektur-ai", "10-Exit-Trade.md") },
|
||||
|
||||
// Infrastruktur
|
||||
{ slug: "news-agent", title: "News Agent", category: "Infrastruktur", icon: "Newspaper", description: "Filter berita ekonomi dan penilaian dampak — saat ini nonaktif", file: path.join(ROOT, "docs", "arsitektur-ai", "11-News-Agent.md") },
|
||||
{ slug: "telegram", title: "Notifikasi Telegram", category: "Infrastruktur", icon: "Send", description: "Notifikasi trade real-time dan ringkasan harian via Telegram Bot", file: path.join(ROOT, "docs", "arsitektur-ai", "12-Telegram-Notifications.md") },
|
||||
{ slug: "auto-trainer", title: "Auto Trainer", category: "Infrastruktur", icon: "RefreshCw", description: "Pipeline retraining otomatis saat kondisi pasar berubah signifikan", file: path.join(ROOT, "docs", "arsitektur-ai", "13-Auto-Trainer.md") },
|
||||
{ slug: "backtest", title: "Backtest", category: "Infrastruktur", icon: "BarChart3", description: "Framework backtesting yang disinkronkan dengan logika live trading", file: path.join(ROOT, "docs", "arsitektur-ai", "14-Backtest.md") },
|
||||
{ slug: "dynamic-confidence", title: "Dynamic Confidence", category: "Infrastruktur", icon: "Gauge", description: "Ambang batas confidence adaptif berdasarkan kondisi dan performa pasar", file: path.join(ROOT, "docs", "arsitektur-ai", "15-Dynamic-Confidence.md") },
|
||||
{ slug: "train-models", title: "Train Models", category: "Infrastruktur", icon: "GraduationCap", description: "Pipeline pelatihan model dan optimasi hyperparameter XGBoost", file: path.join(ROOT, "docs", "arsitektur-ai", "22-Train-Models.md") },
|
||||
|
||||
// Konektor & Konfigurasi
|
||||
{ slug: "mt5-connector", title: "Konektor MT5", category: "Konektor & Konfigurasi", icon: "Plug", description: "Lapisan koneksi MetaTrader 5 dan eksekusi order trading", file: path.join(ROOT, "docs", "arsitektur-ai", "16-MT5-Connector.md") },
|
||||
{ slug: "configuration", title: "Konfigurasi", category: "Konektor & Konfigurasi", icon: "Settings", description: "Pengaturan trading, mode kapital, dan konfigurasi environment", file: path.join(ROOT, "docs", "arsitektur-ai", "17-Configuration.md") },
|
||||
{ slug: "trade-logger", title: "Trade Logger", category: "Konektor & Konfigurasi", icon: "FileText", description: "Pencatatan trade ke database PostgreSQL untuk analisis historis", file: path.join(ROOT, "docs", "arsitektur-ai", "18-Trade-Logger.md") },
|
||||
{ slug: "position-manager", title: "Position Manager", category: "Konektor & Konfigurasi", icon: "ListChecks", description: "Pelacakan dan manajemen posisi terbuka secara real-time", file: path.join(ROOT, "docs", "arsitektur-ai", "19-Position-Manager.md") },
|
||||
|
||||
// Engine & Data
|
||||
{ slug: "risk-engine", title: "Risk Engine", category: "Engine & Data", icon: "Calculator", description: "Perhitungan risiko, Kelly criterion, dan position sizing otomatis", file: path.join(ROOT, "docs", "arsitektur-ai", "20-Risk-Engine.md") },
|
||||
{ slug: "database", title: "Database", category: "Engine & Data", icon: "Database", description: "Skema PostgreSQL dan penyimpanan data perdagangan", file: path.join(ROOT, "docs", "arsitektur-ai", "21-Database.md") },
|
||||
|
||||
// Orkestrator
|
||||
{ slug: "main-live", title: "Orkestrator Utama", category: "Orkestrator", icon: "Play", description: "Async main loop — inti dari trading bot yang mengkoordinasi semua komponen", file: path.join(ROOT, "docs", "arsitektur-ai", "23-Main-Live-Orchestrator.md") },
|
||||
|
||||
// Analisis
|
||||
{ slug: "weakness-analysis", title: "Analisis Kelemahan", category: "Analisis", icon: "AlertTriangle", description: "Kelemahan yang diketahui, risiko, dan prioritas perbaikan sistem", file: path.join(ROOT, "docs", "WEAKNESS_ANALYSIS.md") },
|
||||
];
|
||||
|
||||
function escapeForTemplate(str) {
|
||||
// Escape backticks and ${} in template literals
|
||||
return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
const outDir = path.dirname(OUT);
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
let output = `// AUTO-GENERATED — do not edit manually.
|
||||
// Run: node scripts/generate-books.js
|
||||
|
||||
export interface BookEntry {
|
||||
slug: string;
|
||||
title: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const categories = [
|
||||
"Mulai di Sini",
|
||||
"AI & Analisis",
|
||||
"Risiko & Proteksi",
|
||||
"Proses Trading",
|
||||
"Infrastruktur",
|
||||
"Konektor & Konfigurasi",
|
||||
"Engine & Data",
|
||||
"Orkestrator",
|
||||
"Analisis",
|
||||
] as const;
|
||||
|
||||
export type Category = (typeof categories)[number];
|
||||
|
||||
export const books: BookEntry[] = [\n`;
|
||||
|
||||
for (const def of bookDefs) {
|
||||
let content = "";
|
||||
try {
|
||||
content = fs.readFileSync(def.file, "utf-8");
|
||||
} catch (e) {
|
||||
console.warn(`WARNING: Could not read ${def.file}: ${e.message}`);
|
||||
content = `# ${def.title}\n\n*Dokumen tidak ditemukan.*`;
|
||||
}
|
||||
|
||||
output += ` {
|
||||
slug: ${JSON.stringify(def.slug)},
|
||||
title: ${JSON.stringify(def.title)},
|
||||
category: ${JSON.stringify(def.category)},
|
||||
icon: ${JSON.stringify(def.icon)},
|
||||
description: ${JSON.stringify(def.description)},
|
||||
content: \`${escapeForTemplate(content)}\`,
|
||||
},\n`;
|
||||
}
|
||||
|
||||
output += `];\n`;
|
||||
|
||||
fs.writeFileSync(OUT, output, "utf-8");
|
||||
console.log(`Generated ${OUT} with ${bookDefs.length} books.`);
|
||||
Reference in New Issue
Block a user