Initial commit: quantum_backtester

This commit is contained in:
daavfx
2026-02-08 15:03:27 -06:00
commit b3ce3d043a
55 changed files with 11882 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Data and caches
data/
*.sqlite
*.db
*.env
# OS
Thumbs.db
+31
View File
@@ -0,0 +1,31 @@
# Custom Permission License
Copyright (c) 2025 daavfx
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, subject to the following conditions:
1. The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
2. You may use this Software for any lawful purpose, including personal use,
commercial projects, and modification.
3. You may distribute this Software with your modifications, provided that
you include this license and retain the copyright notice.
**PROHIBITED:**
- Selling, licensing, or monetizing this Software or derivative works
for profit without explicit written permission from the author.
- Using this Software in any way that violates applicable laws.
THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1EXtTZjCp4MfDsEaBf9NUT37yaMmgjSpP
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAAVFX | Trade Replay & Vault</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: #09090b; }
::-webkit-scrollbar-thumb { background: #27272a; border-radius: 2px; }
body { background-color: #09090b; color: #e4e4e7; font-family: 'Inter', -apple-system, sans-serif; -webkit-font-smoothing: antialiased; }
.mono { font-family: 'JetBrains Mono', monospace; }
.glass { background: rgba(24, 24, 27, 0.95); backdrop-filter: blur(20px); }
input[type="range"] { -webkit-appearance: none; background: transparent; }
input[type="range"]::-webkit-slider-track { height: 4px; background: #27272a; border-radius: 2px; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: #3b82f6; border-radius: 50%; margin-top: -5px; cursor: pointer; }
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@^19.2.3",
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
"react/": "https://esm.sh/react@^19.2.3/",
"lucide-react": "https://esm.sh/lucide-react@^0.562.0",
"lightweight-charts": "https://esm.sh/lightweight-charts@4.2.0"
}
}
</script>
<link rel="stylesheet" href="/src/index.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
{
"name": "DAAVFX Trade Replay",
"description": "A high-performance trading replay and vault application built with React, TypeScript, and Lightweight Charts, optimized for Tauri.",
"requestFramePermissions": []
}
Binary file not shown.
+3076
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
{
"name": "daavfx-trade-replay",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2.4.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lightweight-charts": "4.2.0",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"recharts": "^3.6.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"uplot": "^1.6.32"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@tauri-apps/cli": "^2.9.6",
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
View File
+4
View File
@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas
+5050
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.5.3", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.9.5", features = [] }
tauri-plugin-log = "2"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+16
View File
@@ -0,0 +1,16 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "quantum-bt-daavfx",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1431",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "Quantum BT Daavfx",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
Binary file not shown.
+734
View File
@@ -0,0 +1,734 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Save, Settings, Archive, ChevronDown, Calendar,
Camera, Maximize2, SkipBack, ChevronLeft, Play, Pause,
ChevronRight, SkipForward, BookmarkPlus, BookOpen,
CandlestickChart, BarChart2, LineChart, AreaChart,
LayoutTemplate, Sparkles, Check, Zap, Cpu, Monitor
} from 'lucide-react';
import Sidebar from './components/Sidebar';
import Chart, { ChartRef } from './components/Chart';
import VaultModal from './components/VaultModal';
import JournalModal from './components/JournalModal';
import SaveModal from './components/SaveModal';
import QuantumLab from './components/QuantumLab';
import Toast, { ToastType } from './components/Toast';
import { OHLCData, VolumeData, VaultItem, ToolType, ChartType, SessionStats, StrategyConfig, BacktestResult, BacktestSettings } from './types';
import { generateOHLCData, generateVolumeData } from './utils/dataGenerator';
import { onJobComplete, onJobError, onJobProgress, startBacktest } from './tauri/quantumBridge';
const App: React.FC = () => {
// App Mode: Manual Replay vs Quantum Backtest
const [appMode, setAppMode] = useState<'manual' | 'quantum'>('manual');
// State
const [currentTool, setCurrentTool] = useState<ToolType>('crosshair');
const [chartType, setChartType] = useState<ChartType>('Candle');
const [isVaultOpen, setIsVaultOpen] = useState(false);
const [isJournalOpen, setIsJournalOpen] = useState(false);
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const [currentIndex, setCurrentIndex] = useState(50);
const [data] = useState<OHLCData[]>(() => generateOHLCData(1000));
const [volumeData] = useState<VolumeData[]>(() => generateVolumeData(data));
const [toast, setToast] = useState<{message: string, type: ToastType} | null>(null);
// Indicators & Templates State
const [openMenu, setOpenMenu] = useState<'indicators' | 'templates' | null>(null);
const [activeIndicators, setActiveIndicators] = useState<string[]>(['volume', 'ema20']);
const [currentTemplate, setCurrentTemplate] = useState<string>('standard');
// Trade Simulation State
const [lotSize, setLotSize] = useState(0.10);
const [stopLoss, setStopLoss] = useState("1.08339");
const [takeProfit, setTakeProfit] = useState("1.09089");
// Session Stats
const [sessionStats, setSessionStats] = useState<SessionStats>({
balance: 10420,
startBalance: 10000,
winRate: 62.5,
tradesCount: 24,
wins: 15,
losses: 9,
pnl: 420
});
// Quantum Strategy State
const [strategyConfig, setStrategyConfig] = useState<StrategyConfig>({
name: 'Quantum RSI Reversal',
entryConditions: [
{ id: '1', indicator: 'RSI', operator: '<', value: 30, param1: 14 }
],
exitConditions: [],
stopLossPips: 20,
takeProfitPips: 40
});
const [isQuantumRunning, setIsQuantumRunning] = useState(false);
const [quantumResults, setQuantumResults] = useState<BacktestResult | null>(null);
const [quantumJobId, setQuantumJobId] = useState<string | null>(null);
const [quantumProgress, setQuantumProgress] = useState<number>(0);
const [vaultItems, setVaultItems] = useState<VaultItem[]>([
{
id: 1,
title: "EUR/USD London Session Breakout",
category: "drawing",
asset: "EURUSD",
timeframe: "M15",
tags: ["breakout", "support"],
notes: "Clean break of Asian session high.",
createdAt: "2024-02-15T10:30:00",
thumbnail: null,
drawingsCount: 5,
color: "blue"
}
]);
// Data Constants
const INDICATORS = [
{ id: 'volume', label: 'Volume' },
{ id: 'ema20', label: 'EMA 20' },
{ id: 'ema50', label: 'EMA 50' },
{ id: 'rsi', label: 'RSI (14)' },
{ id: 'bollinger', label: 'Bollinger Bands' },
{ id: 'macd', label: 'MACD' },
{ id: 'vp', label: 'Volume Profile' },
];
const TEMPLATES = [
{ id: 'standard', label: 'Standard' },
{ id: 'clean', label: 'Price Action (Clean)' },
{ id: 'smc', label: 'Smart Money (SMC)' },
{ id: 'scalp', label: 'Scalping (1m/5m)' },
{ id: 'swing', label: 'Swing Trading' },
];
const chartRef = useRef<ChartRef>(null);
// Keyboard Shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Only trigger if not typing in an input
if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) return;
switch(e.code) {
case 'Space':
e.preventDefault();
if(appMode === 'manual') setIsPlaying(prev => !prev);
break;
case 'ArrowLeft':
e.preventDefault();
if(appMode === 'manual') {
setCurrentIndex(prev => Math.max(0, prev - 1));
setIsPlaying(false);
}
break;
case 'ArrowRight':
e.preventDefault();
if(appMode === 'manual') {
setCurrentIndex(prev => Math.min(data.length - 1, prev + 1));
setIsPlaying(false);
}
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [data.length, appMode]);
// Playback Logic
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
if (isPlaying && appMode === 'manual') {
interval = setInterval(() => {
setCurrentIndex(prev => {
if (prev >= data.length - 1) {
setIsPlaying(false);
return prev;
}
return prev + 1;
});
}, 1000 / playbackSpeed);
}
return () => clearInterval(interval);
}, [isPlaying, playbackSpeed, data.length, appMode]);
const showToast = (message: string, type: ToastType = 'success') => {
setToast({ message, type });
};
const handleSaveSession = (saveData: any) => {
const newItem: VaultItem = {
id: Date.now(),
title: saveData.title,
category: saveData.category,
asset: 'EURUSD',
timeframe: 'M15',
tags: saveData.tags,
notes: saveData.notes,
createdAt: new Date().toISOString(),
thumbnail: null,
drawingsCount: Math.floor(Math.random() * 10),
color: 'emerald'
};
setVaultItems([newItem, ...vaultItems]);
setIsSaveModalOpen(false);
showToast("Session saved to vault successfully!");
};
const handleQuickSave = () => {
const newItem: VaultItem = {
id: Date.now(),
title: `Quick Save - ${new Date().toLocaleString()}`,
category: 'replay',
asset: 'EURUSD',
timeframe: 'M15',
tags: ['quick'],
notes: 'Auto-saved from replay',
createdAt: new Date().toISOString(),
thumbnail: null,
drawingsCount: 0,
color: 'blue'
};
setVaultItems([newItem, ...vaultItems]);
showToast("Quick save added to vault!");
};
const handleDeleteVaultItem = (id: number) => {
setVaultItems(prev => prev.filter(i => i.id !== id));
showToast("Item deleted from vault", "info");
};
const toggleFullscreen = async () => {
if (!document.fullscreenElement) {
await document.documentElement.requestFullscreen();
} else {
await document.exitFullscreen();
}
};
const executeTrade = (type: 'LONG' | 'SHORT') => {
const currentPrice = data[currentIndex].close;
const entryPrice = currentPrice;
const isWin = Math.random() > 0.5;
const pnl = isWin ? (lotSize * 100) : -(lotSize * 50);
setSessionStats(prev => ({
...prev,
tradesCount: prev.tradesCount + 1,
wins: prev.wins + (isWin ? 1 : 0),
losses: prev.losses + (isWin ? 0 : 1),
balance: prev.balance + pnl,
pnl: prev.pnl + pnl,
winRate: Math.round(((prev.wins + (isWin ? 1 : 0)) / (prev.tradesCount + 1)) * 100)
}));
showToast(`${type} Executed @ ${entryPrice.toFixed(5)} (${isWin ? 'Win' : 'Loss'})`, isWin ? 'success' : 'error');
};
const toggleIndicator = (id: string) => {
setActiveIndicators(prev => {
const isActive = prev.includes(id);
const newIndicators = isActive ? prev.filter(i => i !== id) : [...prev, id];
showToast(isActive ? `${INDICATORS.find(i=>i.id===id)?.label} Removed` : `${INDICATORS.find(i=>i.id===id)?.label} Added`, 'info');
return newIndicators;
});
};
const applyTemplate = (id: string) => {
setCurrentTemplate(id);
setOpenMenu(null);
showToast(`Template applied: ${TEMPLATES.find(t=>t.id===id)?.label}`, 'success');
};
const runQuantumBacktestMock = () => {
setIsQuantumRunning(true);
setQuantumProgress(0);
setTimeout(() => {
const curve = [];
let balance = 10000;
const now = Math.floor(Date.now() / 1000) - (86400 * 30);
for(let i=0; i<30; i++) {
const change = (Math.random() - 0.4) * 500;
balance += change;
curve.push({ time: now + (i * 86400), value: balance });
}
// Mock Data Generator for Detailed Stats
setQuantumResults({
// Core
totalTrades: 30,
netProfit: -1.12,
profitFactor: 0.99,
maxDrawdown: 15.82,
maxDrawdownPercent: 0.16,
sharpeRatio: 1.05,
equityCurve: curve,
trades: Array(5).fill(null).map((_, i) => ({
id: i.toString(),
pair: 'XAUUSD',
type: Math.random() > 0.5 ? 'LONG' : 'SHORT',
time: '09:00',
entry: 2025.50 + (i*0.5),
exit: 2027.00 + (i*0.5),
lots: 0.1,
pnl: (Math.random() - 0.5) * 20,
r: 1.5,
status: Math.random() > 0.5 ? 'WIN' : 'LOSS',
setup: 'Algo'
})),
// Detailed
initialDeposit: 10000.00,
grossProfit: 88.07,
grossLoss: -89.19,
expectedPayoff: -0.04,
absoluteDrawdown: 14.86,
relativeDrawdown: 15.82,
relativeDrawdownPercent: 0.16,
shortPositions: 18,
shortWon: 10,
longPositions: 12,
longWon: 10,
profitTrades: 20,
lossTrades: 10,
largestProfitTrade: 16.61,
largestLossTrade: -22.63,
averageProfitTrade: 4.40,
averageLossTrade: -8.92,
maxConsecutiveWins: 6,
maxConsecutiveWinsValue: 23.26,
maxConsecutiveLosses: 3,
maxConsecutiveLossesValue: -36.88,
ticksModelled: 3224126,
modellingQuality: 90.00
});
setIsQuantumRunning(false);
showToast("Quantum Backtest Complete", "success");
}, 1500);
};
useEffect(() => {
const unsubs: Array<() => void> = [];
(async () => {
try {
const unProgress = await onJobProgress((e) => {
if (quantumJobId && e.jobId !== quantumJobId) return;
setQuantumProgress(e.progress);
});
unsubs.push(() => unProgress());
const unComplete = await onJobComplete((e) => {
if (quantumJobId && e.jobId !== quantumJobId) return;
setQuantumResults(e.result);
setIsQuantumRunning(false);
setQuantumProgress(100);
showToast("Quantum Backtest Complete", "success");
});
unsubs.push(() => unComplete());
const unError = await onJobError((e) => {
if (quantumJobId && e.jobId !== quantumJobId) return;
setIsQuantumRunning(false);
showToast(e.error, "error");
});
unsubs.push(() => unError());
} catch {
}
})();
return () => {
for (const u of unsubs) u();
};
}, [quantumJobId]);
const runQuantumBacktest = async (settings: BacktestSettings) => {
if (!settings.datasetId) {
showToast("Load a CSV dataset before running a backtest.", "error");
return;
}
setIsQuantumRunning(true);
setQuantumProgress(0);
setQuantumResults(null);
try {
const { jobId } = await startBacktest({
settings,
strategy: strategyConfig,
});
setQuantumJobId(jobId);
showToast(`Backtest queued (job ${jobId.slice(0, 8)})`, "info");
} catch (e) {
setIsQuantumRunning(false);
showToast(String(e), "error");
runQuantumBacktestMock();
}
};
return (
<div className="min-h-screen overflow-hidden flex flex-col bg-zinc-950 text-zinc-200 font-sans">
{toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
{/* Backdrop for menus */}
{openMenu && (
<div className="fixed inset-0 z-[55]" onClick={() => setOpenMenu(null)} />
)}
{/* Header */}
<header data-tauri-drag-region="true" className="fixed top-0 left-0 right-0 z-50 glass border-b border-zinc-800/50 h-11 select-none">
<div className="flex items-center justify-between h-full px-3">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 pointer-events-none">
<span className="text-sm font-semibold tracking-tighter text-white">DAAVFX</span>
<span className="text-xs text-zinc-600">|</span>
<span className="text-xs text-zinc-500 tracking-tight">RYIUK</span>
</div>
<div className="hidden md:flex items-center gap-1 ml-4 bg-zinc-900 rounded p-0.5 border border-zinc-800">
<button
onClick={() => setAppMode('manual')}
className={`flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded transition-all ${
appMode === 'manual'
? 'bg-zinc-800 text-white shadow-sm'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
<Monitor size={12} /> Manual
</button>
<button
onClick={() => setAppMode('quantum')}
className={`flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded transition-all ${
appMode === 'quantum'
? 'bg-slate-800 text-slate-200 shadow-sm border border-slate-700'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
<Zap size={12} /> Quantum Lab
</button>
</div>
<div className="hidden md:flex items-center gap-1 ml-2">
<button onClick={() => setIsVaultOpen(true)} className="px-2.5 py-1 text-xs font-medium text-zinc-400 hover:text-white hover:bg-zinc-800/50 rounded transition-colors flex items-center gap-1.5">
<Archive size={12} /> Vault
</button>
<button onClick={() => setIsJournalOpen(true)} className="px-2.5 py-1 text-xs font-medium text-zinc-400 hover:text-white hover:bg-zinc-800/50 rounded transition-colors flex items-center gap-1.5">
<BookOpen size={12} /> Journal
</button>
</div>
</div>
<div className="flex items-center gap-3">
<button onClick={() => setIsSaveModalOpen(true)} className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-zinc-300 bg-zinc-800 hover:bg-zinc-700 rounded transition-colors">
<Save size={12} /> Save
</button>
<div className="flex items-center gap-1.5 pl-3 border-l border-zinc-800">
<div className="w-3 h-3 rounded-full bg-yellow-500/20 hover:bg-yellow-500 transition-colors cursor-pointer"></div>
<div className="w-3 h-3 rounded-full bg-emerald-500/20 hover:bg-emerald-500 transition-colors cursor-pointer"></div>
<div className="w-3 h-3 rounded-full bg-red-500/20 hover:bg-red-500 transition-colors cursor-pointer"></div>
</div>
</div>
</div>
</header>
{/* Main Content Area */}
{appMode === 'quantum' ? (
// --- QUANTUM LAB LAYOUT ---
<main className="pt-11 h-screen flex flex-col">
<QuantumLab
strategyConfig={strategyConfig}
setStrategyConfig={setStrategyConfig}
quantumResults={quantumResults}
runBacktest={runQuantumBacktest}
isRunning={isQuantumRunning}
/>
</main>
) : (
// --- MANUAL REPLAY LAYOUT ---
<main className="pt-11 h-screen flex">
<Sidebar currentTool={currentTool} setTool={setCurrentTool} onDeleteAll={() => showToast("All drawings cleared", "info")} />
<div className="flex-1 flex flex-col">
{/* Toolbar */}
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/50 bg-zinc-950/50">
<div className="flex items-center gap-3">
<div className="relative">
<button className="flex items-center gap-2 px-3 py-1.5 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 rounded transition-colors">
<span className="text-sm font-semibold text-white">EUR/USD</span>
<ChevronDown size={14} className="text-zinc-500" />
</button>
</div>
<div className="flex items-center bg-zinc-900 border border-zinc-800 rounded overflow-hidden">
{['M1','M5','M15','H1','H4','D1'].map(tf => (
<button key={tf} className={`px-2.5 py-1.5 text-xs font-medium transition-colors ${tf === 'M15' ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}`}>{tf}</button>
))}
</div>
<div className="flex items-center bg-zinc-900 border border-zinc-800 rounded overflow-hidden">
<button onClick={() => setChartType('Candle')} className={`px-2 py-1.5 transition-colors ${chartType === 'Candle' ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}`} title="Candles"><CandlestickChart size={14} /></button>
<button onClick={() => setChartType('Bar')} className={`px-2 py-1.5 transition-colors ${chartType === 'Bar' ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}`} title="Bars"><BarChart2 size={14} /></button>
<button onClick={() => setChartType('Line')} className={`px-2 py-1.5 transition-colors ${chartType === 'Line' ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}`} title="Line"><LineChart size={14} /></button>
<button onClick={() => setChartType('Area')} className={`px-2 py-1.5 transition-colors ${chartType === 'Area' ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}`} title="Area"><AreaChart size={14} /></button>
</div>
</div>
<div className="flex items-center gap-2 relative">
<div className="relative">
<button
onClick={() => setOpenMenu(openMenu === 'indicators' ? null : 'indicators')}
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium border rounded transition-colors ${openMenu === 'indicators' ? 'text-white bg-zinc-800 border-zinc-700' : 'text-zinc-400 hover:text-white bg-zinc-900 border-zinc-800 hover:border-zinc-700'}`}
>
<Sparkles size={13} /> Indicators
</button>
{openMenu === 'indicators' && (
<div className="absolute top-full right-0 mt-1 w-56 bg-zinc-900 border border-zinc-800 rounded-lg shadow-xl z-[60] py-1 animate-fadeIn">
<div className="px-3 py-2 border-b border-zinc-800 text-xs font-semibold text-zinc-500 uppercase">Available Indicators</div>
{INDICATORS.map(ind => (
<button
key={ind.id}
onClick={() => toggleIndicator(ind.id)}
className="flex items-center justify-between w-full px-3 py-2 text-xs hover:bg-zinc-800 text-left transition-colors"
>
<span className={activeIndicators.includes(ind.id) ? 'text-white' : 'text-zinc-400'}>{ind.label}</span>
{activeIndicators.includes(ind.id) && <Check size={12} className="text-blue-500" />}
</button>
))}
</div>
)}
</div>
<div className="relative">
<button
onClick={() => setOpenMenu(openMenu === 'templates' ? null : 'templates')}
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium border rounded transition-colors ${openMenu === 'templates' ? 'text-white bg-zinc-800 border-zinc-700' : 'text-zinc-400 hover:text-white bg-zinc-900 border-zinc-800 hover:border-zinc-700'}`}
>
<LayoutTemplate size={13} /> Templates
</button>
{openMenu === 'templates' && (
<div className="absolute top-full right-0 mt-1 w-56 bg-zinc-900 border border-zinc-800 rounded-lg shadow-xl z-[60] py-1 animate-fadeIn">
<div className="px-3 py-2 border-b border-zinc-800 text-xs font-semibold text-zinc-500 uppercase">Chart Templates</div>
{TEMPLATES.map(temp => (
<button
key={temp.id}
onClick={() => applyTemplate(temp.id)}
className="flex items-center justify-between w-full px-3 py-2 text-xs hover:bg-zinc-800 text-left transition-colors"
>
<span className={currentTemplate === temp.id ? 'text-white' : 'text-zinc-400'}>{temp.label}</span>
{currentTemplate === temp.id && <Check size={12} className="text-emerald-500" />}
</button>
))}
</div>
)}
</div>
<button className="p-1.5 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded transition-colors" title="Screenshot">
<Camera size={15} />
</button>
<button onClick={toggleFullscreen} className="p-1.5 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded transition-colors" title="Fullscreen">
<Maximize2 size={15} />
</button>
</div>
</div>
<div className="flex-1 flex overflow-hidden">
<div className="flex-1 flex flex-col min-w-0 relative">
<div className="flex-1 flex flex-col">
<Chart
ref={chartRef}
data={data}
volumeData={volumeData}
currentIndex={currentIndex}
onIndexChange={setCurrentIndex}
currentTool={currentTool}
chartType={chartType}
/>
<div className="px-4 py-3 border-t border-zinc-800/50 bg-zinc-950/80">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<button onClick={() => setCurrentIndex(0)} className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded" title="Start"><SkipBack size={16} /></button>
<button onClick={() => setCurrentIndex(Math.max(0, currentIndex - 1))} className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded" title="Previous Candle"><ChevronLeft size={16} /></button>
<button onClick={() => setIsPlaying(!isPlaying)} className="p-2.5 text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors" title="Play/Pause">
{isPlaying ? <Pause size={18} fill="currentColor" /> : <Play size={18} fill="currentColor" />}
</button>
<button onClick={() => setCurrentIndex(Math.min(data.length - 1, currentIndex + 1))} className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded" title="Next Candle"><ChevronRight size={16} /></button>
<button onClick={() => setCurrentIndex(data.length - 1)} className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded" title="End"><SkipForward size={16} /></button>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500">Speed</span>
<div className="flex items-center bg-zinc-900 border border-zinc-800 rounded overflow-hidden">
{[0.5, 1, 2, 5, 10].map(speed => (
<button
key={speed}
onClick={() => setPlaybackSpeed(speed)}
className={`px-2 py-1 text-xs transition-colors ${playbackSpeed === speed ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white hover:bg-zinc-800'}`}
>
{speed}x
</button>
))}
</div>
</div>
<div className="flex-1 flex items-center gap-3">
<span className="text-xs mono text-zinc-500">{currentIndex}</span>
<input
type="range"
min="0"
max={data.length - 1}
value={currentIndex}
onChange={(e) => { setIsPlaying(false); setCurrentIndex(Number(e.target.value)); }}
className="flex-1 cursor-pointer"
/>
<span className="text-xs mono text-zinc-500">{data.length - 1}</span>
</div>
<button onClick={handleQuickSave} className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-500 rounded transition-colors">
<BookmarkPlus size={14} /> Quick Save
</button>
</div>
</div>
</div>
</div>
<aside className="w-72 border-l border-zinc-800/50 flex flex-col bg-zinc-950/30">
<div className="p-3 border-b border-zinc-800/50">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-zinc-300">Place Trade</span>
<div className="flex items-center gap-1">
<button onClick={() => executeTrade('LONG')} className="px-2 py-1 text-xs text-emerald-400 bg-emerald-500/10 rounded hover:bg-emerald-500/20 transition-colors">Buy</button>
<button onClick={() => executeTrade('SHORT')} className="px-2 py-1 text-xs text-zinc-400 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors">Sell</button>
</div>
</div>
<div className="space-y-3">
<div>
<label className="text-xs text-zinc-500 mb-1.5 block">Lot Size</label>
<div className="flex items-center gap-2">
<input
type="number"
value={lotSize}
onChange={(e) => setLotSize(Number(e.target.value))}
step="0.01"
min="0.01"
className="flex-1 h-8 px-3 text-xs mono bg-zinc-900 border border-zinc-800 rounded text-white focus:outline-none focus:border-zinc-600"
/>
<div className="flex gap-1">
<button onClick={() => setLotSize(0.1)} className="px-2 py-1.5 text-xs text-zinc-400 bg-zinc-800 hover:bg-zinc-700 rounded transition-colors">0.1</button>
<button onClick={() => setLotSize(0.5)} className="px-2 py-1.5 text-xs text-zinc-400 bg-zinc-800 hover:bg-zinc-700 rounded transition-colors">0.5</button>
<button onClick={() => setLotSize(1.0)} className="px-2 py-1.5 text-xs text-zinc-400 bg-zinc-800 hover:bg-zinc-700 rounded transition-colors">1.0</button>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-xs text-zinc-500 mb-1.5 block">Stop Loss</label>
<input
type="text"
value={stopLoss}
onChange={(e) => setStopLoss(e.target.value)}
className="w-full h-8 px-3 text-xs mono bg-zinc-900 border border-zinc-800 rounded text-white focus:outline-none focus:border-zinc-600"
/>
</div>
<div>
<label className="text-xs text-zinc-500 mb-1.5 block">Take Profit</label>
<input
type="text"
value={takeProfit}
onChange={(e) => setTakeProfit(e.target.value)}
className="w-full h-8 px-3 text-xs mono bg-zinc-900 border border-zinc-800 rounded text-white focus:outline-none focus:border-zinc-600"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2 mt-2">
<button onClick={() => executeTrade('LONG')} className="w-full py-2.5 text-xs font-medium text-white bg-emerald-600 hover:bg-emerald-500 rounded-md transition-colors shadow-lg shadow-emerald-900/20">
Buy Market
</button>
<button onClick={() => executeTrade('SHORT')} className="w-full py-2.5 text-xs font-medium text-white bg-red-600 hover:bg-red-500 rounded-md transition-colors shadow-lg shadow-red-900/20">
Sell Market
</button>
</div>
</div>
</div>
<div className="p-3 border-b border-zinc-800/50">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-zinc-300">Session Stats</span>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
<div className="flex justify-between text-xs">
<span className="text-zinc-500">Balance</span>
<span className="mono text-white">${sessionStats.balance.toLocaleString()}</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-zinc-500">Win Rate</span>
<span className="mono text-emerald-400">{sessionStats.winRate}%</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-zinc-500">Trades</span>
<span className="mono text-white">{sessionStats.tradesCount}</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-zinc-500">P/L</span>
<span className={`mono ${sessionStats.pnl >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{sessionStats.pnl >= 0 ? '+' : ''}${sessionStats.pnl}
</span>
</div>
</div>
</div>
<div className="flex-1 overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-800/50">
<span className="text-xs font-medium text-zinc-300">Recent Vault Items</span>
<button onClick={() => setIsVaultOpen(true)} className="text-xs text-blue-400 hover:text-blue-300 transition-colors">View All</button>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{vaultItems.slice(0, 5).map(item => (
<div key={item.id} onClick={() => setIsVaultOpen(true)} className="p-2 bg-zinc-800/30 border border-zinc-800/50 rounded-lg cursor-pointer hover:bg-zinc-800/50 transition-colors">
<div className="flex items-center gap-2 mb-1">
<span className={`w-1.5 h-1.5 bg-${item.color}-500 rounded-full`}></span>
<span className="text-xs font-medium text-white truncate">{item.title}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500">{item.asset}</span>
<span className="text-xs text-zinc-500">{new Date(item.createdAt).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
</div>
</aside>
</div>
</div>
</main>
)}
<VaultModal
isOpen={isVaultOpen}
onClose={() => setIsVaultOpen(false)}
items={vaultItems}
onLoad={() => { setIsVaultOpen(false); showToast("Session loaded!"); }}
onDelete={handleDeleteVaultItem}
onNew={() => { setIsVaultOpen(false); setIsSaveModalOpen(true); }}
/>
<JournalModal
isOpen={isJournalOpen}
onClose={() => setIsJournalOpen(false)}
/>
<SaveModal
isOpen={isSaveModalOpen}
onClose={() => setIsSaveModalOpen(false)}
onSave={handleSaveSession}
/>
</div>
);
};
export default App;
+341
View File
@@ -0,0 +1,341 @@
import React, { useState } from 'react';
import {
Settings, FileCode, Play, Monitor, Layers, Calendar,
Database, Activity, Zap, HardDrive, Cpu, CheckCircle,
Network, Server, Wifi, Sliders
} from 'lucide-react';
import { BacktestSettings } from '../types';
interface BacktestConfigPanelProps {
onRun: (settings: BacktestSettings) => void;
isRunning: boolean;
}
const BacktestConfigPanel: React.FC<BacktestConfigPanelProps> = ({ onRun, isRunning }) => {
const [settings, setSettings] = useState<BacktestSettings>({
algoFile: 'Quantum_RSI_Strategy_v4.rs',
algoLanguage: 'Rust',
symbol: 'EURUSD',
timeframe: 'M15',
dateRange: 'Custom',
startDate: '2025-01-01',
endDate: '2025-02-01',
forwardMode: 'No',
modeling: 'Every Tick',
latency: 'Zero',
optimization: 'Disabled',
deposit: 10000,
leverage: 100,
visualMode: false,
visualSpeed: 100
});
const handleRun = () => {
onRun(settings);
};
const FileIcon = ({ lang }: { lang: string }) => {
switch(lang) {
case 'Rust': return <div className="text-orange-500 font-bold text-[10px]">RS</div>;
case 'Python': return <div className="text-blue-400 font-bold text-[10px]">PY</div>;
case 'C++': return <div className="text-blue-600 font-bold text-[10px]">CPP</div>;
default: return <div className="text-zinc-400 font-bold text-[10px]">{lang}</div>;
}
};
const AgentNode = ({ name, type, usage, status }: any) => (
<div className="flex items-center justify-between p-2 bg-zinc-950 border border-zinc-800 rounded mb-1">
<div className="flex items-center gap-2">
{type === 'local' ? <Cpu size={14} className="text-zinc-500" /> : <Server size={14} className="text-blue-500" />}
<span className="text-xs text-zinc-300">{name}</span>
</div>
<div className="flex items-center gap-3">
<div className="w-24 h-1.5 bg-zinc-800 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${status === 'Busy' ? 'bg-emerald-500' : 'bg-zinc-600'}`} style={{ width: `${usage}%` }}></div>
</div>
<span className={`text-[10px] w-8 text-right ${status === 'Busy' ? 'text-emerald-400' : 'text-zinc-500'}`}>{status}</span>
</div>
</div>
);
return (
<div className="flex flex-col h-full bg-zinc-900/30 p-6 overflow-y-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Settings className="text-blue-500" />
Strategy Tester Configuration
</h2>
<p className="text-sm text-zinc-500">Configure simulation environment, load algorithms, and set execution parameters.</p>
</div>
{/* Visual Mode Toggle (Big Switch) */}
<div className="flex items-center gap-4 bg-zinc-900 border border-zinc-800 p-2 rounded-xl">
<div className="text-right">
<div className="text-xs font-semibold text-white">Visual Mode</div>
<div className="text-[10px] text-zinc-500">{settings.visualMode ? 'Chart Replay' : 'Headless (Fast)'}</div>
</div>
<button
onClick={() => setSettings({...settings, visualMode: !settings.visualMode})}
className={`w-12 h-6 rounded-full relative transition-colors ${settings.visualMode ? 'bg-blue-600' : 'bg-zinc-700'}`}
>
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${settings.visualMode ? 'left-7' : 'left-1'}`}></div>
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* SECTION 1: EXPERT ADVISOR */}
<div className="space-y-4">
<h3 className="text-xs font-semibold text-zinc-500 uppercase flex items-center gap-2">
<Cpu size={14} /> Expert Advisor (Algo)
</h3>
<div className="p-5 bg-zinc-900/50 border border-zinc-800 rounded-xl space-y-4 hover:border-zinc-700 transition-colors">
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Strategy File</label>
<div className="flex items-center gap-2">
<div className="flex-1 h-10 bg-zinc-950 border border-zinc-800 rounded-lg flex items-center px-3 gap-3">
<FileCode size={16} className="text-zinc-500" />
<span className="text-sm text-white font-mono">{settings.algoFile}</span>
<div className="ml-auto px-1.5 py-0.5 bg-zinc-900 border border-zinc-800 rounded flex items-center justify-center">
<FileIcon lang={settings.algoLanguage} />
</div>
</div>
<button className="h-10 px-4 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-lg text-xs font-medium text-white transition-colors">
Load
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Optimization</label>
<select
value={settings.optimization}
onChange={(e) => setSettings({...settings, optimization: e.target.value as any})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500"
>
<option value="Disabled">Disabled</option>
<option value="Fast Genetic">Fast Genetic Algorithm</option>
<option value="Slow Complete">Slow Complete Algorithm</option>
</select>
</div>
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Language Runtime</label>
<select
value={settings.algoLanguage}
onChange={(e) => setSettings({...settings, algoLanguage: e.target.value as any})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500"
>
<option value="Rust">Rust (Native Speed)</option>
<option value="Python">Python (PyO3 Bridge)</option>
<option value="C++">C++ (WASM)</option>
<option value="JSON">No-Code (JSON)</option>
</select>
</div>
</div>
</div>
</div>
{/* SECTION 2: MARKET DATA */}
<div className="space-y-4">
<h3 className="text-xs font-semibold text-zinc-500 uppercase flex items-center gap-2">
<Database size={14} /> Market & Data
</h3>
<div className="p-5 bg-zinc-900/50 border border-zinc-800 rounded-xl space-y-4 hover:border-zinc-700 transition-colors">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Symbol / Universe</label>
<div className="relative">
<select
value={settings.symbol}
onChange={(e) => setSettings({...settings, symbol: e.target.value})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500 appearance-none"
>
<option value="EURUSD">EURUSD</option>
<option value="GBPUSD">GBPUSD</option>
<option value="XAUUSD">XAUUSD (Gold)</option>
<option value="Multi-Pair">Multi-Pair Portfolio</option>
</select>
<Layers size={14} className="absolute right-3 top-2.5 text-zinc-500 pointer-events-none" />
</div>
</div>
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Timeframe</label>
<select
value={settings.timeframe}
onChange={(e) => setSettings({...settings, timeframe: e.target.value})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500"
>
<option value="M1">M1 (1 Minute)</option>
<option value="M5">M5 (5 Minutes)</option>
<option value="M15">M15 (15 Minutes)</option>
<option value="H1">H1 (1 Hour)</option>
<option value="H4">H4 (4 Hours)</option>
<option value="D1">D1 (Daily)</option>
</select>
</div>
</div>
{/* Date Selection */}
<div className="grid grid-cols-3 gap-4">
<div className="col-span-1">
<label className="text-xs text-zinc-400 mb-1.5 block">Period</label>
<select
value={settings.dateRange}
onChange={(e) => setSettings({...settings, dateRange: e.target.value as any})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500"
>
<option value="Last Month">Last Month</option>
<option value="Last Year">Last Year</option>
<option value="Custom">Custom Range</option>
</select>
</div>
<div className="col-span-1">
<label className="text-xs text-zinc-400 mb-1.5 block">Start Date</label>
<div className="relative">
<input
type="date"
value={settings.startDate}
onChange={(e) => setSettings({...settings, startDate: e.target.value})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-xs text-white px-2 focus:outline-none focus:border-blue-500"
/>
</div>
</div>
<div className="col-span-1">
<label className="text-xs text-zinc-400 mb-1.5 block">End Date</label>
<div className="relative">
<input
type="date"
value={settings.endDate}
onChange={(e) => setSettings({...settings, endDate: e.target.value})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-xs text-white px-2 focus:outline-none focus:border-blue-500"
/>
</div>
</div>
</div>
</div>
</div>
{/* SECTION 3: ENVIRONMENT & EXECUTION */}
<div className="space-y-4">
<h3 className="text-xs font-semibold text-zinc-500 uppercase flex items-center gap-2">
<Network size={14} /> Environment & Execution
</h3>
<div className="p-5 bg-zinc-900/50 border border-zinc-800 rounded-xl space-y-4 hover:border-zinc-700 transition-colors">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Initial Deposit ($)</label>
<input
type="number"
value={settings.deposit}
onChange={(e) => setSettings({...settings, deposit: Number(e.target.value)})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-3 focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Simulated Latency</label>
<div className="relative">
<select
value={settings.latency}
onChange={(e) => setSettings({...settings, latency: e.target.value as any})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500"
>
<option value="Zero">Zero Latency (Ideal)</option>
<option value="Random (1-10ms)">Random (1-10ms)</option>
<option value="Network (50-200ms)">Network (50-200ms)</option>
</select>
<Wifi size={14} className="absolute right-3 top-2.5 text-zinc-500 pointer-events-none" />
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Forward Mode</label>
<select
value={settings.forwardMode}
onChange={(e) => setSettings({...settings, forwardMode: e.target.value as any})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500"
>
<option value="No">No</option>
<option value="1/2">1/2 (50%)</option>
<option value="1/3">1/3 (33%)</option>
<option value="1/4">1/4 (25%)</option>
</select>
</div>
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Modeling</label>
<select
value={settings.modeling}
onChange={(e) => setSettings({...settings, modeling: e.target.value as any})}
className="w-full h-9 bg-zinc-950 border border-zinc-800 rounded-lg text-sm text-white px-2 focus:outline-none focus:border-blue-500"
>
<option value="Every Tick">Every Tick</option>
<option value="OHLC (Fast)">OHLC</option>
<option value="Open Prices Only">Open Only</option>
</select>
</div>
</div>
</div>
</div>
{/* SECTION 4: DISTRIBUTED AGENTS */}
<div className="space-y-4">
<h3 className="text-xs font-semibold text-zinc-500 uppercase flex items-center gap-2">
<Monitor size={14} /> Distributed Agents
</h3>
<div className="p-5 bg-zinc-900/50 border border-zinc-800 rounded-xl space-y-2 hover:border-zinc-700 transition-colors h-[170px] overflow-y-auto">
<div className="flex items-center justify-between mb-2">
<span className="text-[10px] text-zinc-500 uppercase">Local Cores</span>
<span className="text-[10px] text-emerald-500">8 Active</span>
</div>
<AgentNode name="Core 1 (Local)" type="local" usage={85} status="Busy" />
<AgentNode name="Core 2 (Local)" type="local" usage={72} status="Busy" />
<AgentNode name="Core 3 (Local)" type="local" usage={12} status="Idle" />
<div className="flex items-center justify-between mt-4 mb-2">
<span className="text-[10px] text-zinc-500 uppercase">Cloud Network</span>
<span className="text-[10px] text-blue-500">12 Available</span>
</div>
<AgentNode name="Cloud-US-East-1" type="cloud" usage={0} status="Ready" />
</div>
</div>
</div>
{/* ACTION BAR */}
<div className="mt-auto pt-6 border-t border-zinc-800">
<div className="flex items-center justify-end gap-4">
<div className="text-right mr-4 hidden sm:block">
<div className="text-xs text-zinc-500">Estimated Duration</div>
<div className="text-sm font-mono text-white">{settings.visualMode ? 'Variable' : '< 1.5s'}</div>
</div>
<button
onClick={handleRun}
disabled={isRunning}
className={`px-8 py-3 rounded-xl flex items-center gap-3 font-bold text-sm uppercase tracking-wider transition-all shadow-xl ${
isRunning
? 'bg-zinc-800 text-zinc-500 cursor-not-allowed'
: 'bg-blue-700 hover:bg-blue-600 text-white shadow-blue-900/20 transform hover:-translate-y-0.5'
}`}
>
{isRunning ? (
<>
<Activity className="animate-spin" size={18} />
Processing...
</>
) : (
<>
<Play size={18} fill="currentColor" />
Start Test
</>
)}
</button>
</div>
</div>
</div>
);
};
export default BacktestConfigPanel;
+275
View File
@@ -0,0 +1,275 @@
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { createChart, ColorType, CrosshairMode, IChartApi, ISeriesApi, LineStyle } from 'lightweight-charts';
import { OHLCData, VolumeData, ChartType } from '../types';
interface ChartProps {
data: OHLCData[];
volumeData: VolumeData[];
currentIndex: number;
onIndexChange: (index: number) => void;
currentTool: string;
chartType: ChartType;
}
export interface ChartRef {
resize: () => void;
}
const Chart = forwardRef<ChartRef, ChartProps>(({ data, volumeData, currentIndex, onIndexChange, currentTool, chartType }, ref) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const volumeContainerRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const chartApiRef = useRef<IChartApi | null>(null);
const volumeApiRef = useRef<IChartApi | null>(null);
const seriesRef = useRef<ISeriesApi<any> | null>(null);
const volumeSeriesRef = useRef<ISeriesApi<"Histogram"> | null>(null);
const [candleInfo, setCandleInfo] = useState<OHLCData | null>(null);
useImperativeHandle(ref, () => ({
resize: () => {
if (chartContainerRef.current && chartApiRef.current) {
chartApiRef.current.applyOptions({
width: chartContainerRef.current.clientWidth,
height: chartContainerRef.current.clientHeight
});
}
if (volumeContainerRef.current && volumeApiRef.current) {
volumeApiRef.current.applyOptions({
width: volumeContainerRef.current.clientWidth,
height: volumeContainerRef.current.clientHeight
});
}
}
}));
// Initialize Chart Instance (Once)
useEffect(() => {
if (!chartContainerRef.current || !volumeContainerRef.current) return;
const chart = createChart(chartContainerRef.current, {
layout: {
background: { type: ColorType.Solid, color: 'transparent' },
textColor: '#71717a',
fontFamily: "'Inter', sans-serif",
},
grid: {
vertLines: { color: '#1f1f23' },
horzLines: { color: '#1f1f23' }
},
crosshair: {
mode: CrosshairMode.Normal,
vertLine: { color: '#3b82f6', width: 1, style: LineStyle.Dashed, labelBackgroundColor: '#18181b' },
horzLine: { color: '#3b82f6', width: 1, style: LineStyle.Dashed, labelBackgroundColor: '#18181b' },
},
rightPriceScale: {
borderColor: '#27272a',
scaleMargins: { top: 0.1, bottom: 0.1 }
},
timeScale: {
borderColor: '#27272a',
timeVisible: true,
secondsVisible: false
},
});
const volumeChart = createChart(volumeContainerRef.current, {
layout: { background: { type: ColorType.Solid, color: 'transparent' }, textColor: '#71717a' },
grid: { vertLines: { visible: false }, horzLines: { color: '#1f1f23' } },
rightPriceScale: { borderColor: '#27272a', visible: false },
timeScale: { visible: false },
});
const volumeSeries = volumeChart.addHistogramSeries({
priceFormat: { type: 'volume' },
priceScaleId: '',
});
volumeSeriesRef.current = volumeSeries;
chart.timeScale().subscribeVisibleLogicalRangeChange(range => {
if (range) volumeChart.timeScale().setVisibleLogicalRange(range);
});
chartApiRef.current = chart;
volumeApiRef.current = volumeChart;
const resizeObserver = new ResizeObserver(() => {
if (chartContainerRef.current && chartApiRef.current) {
chartApiRef.current.applyOptions({
width: chartContainerRef.current.clientWidth,
height: chartContainerRef.current.clientHeight
});
}
if (volumeContainerRef.current && volumeApiRef.current) {
volumeApiRef.current.applyOptions({
width: volumeContainerRef.current.clientWidth,
height: volumeContainerRef.current.clientHeight
});
}
});
if (wrapperRef.current) {
resizeObserver.observe(wrapperRef.current);
}
return () => {
resizeObserver.disconnect();
try {
chart.remove();
} catch (e) {
console.warn('Failed to remove chart:', e);
}
try {
volumeChart.remove();
} catch (e) {
console.warn('Failed to remove volume chart:', e);
}
chartApiRef.current = null;
volumeApiRef.current = null;
seriesRef.current = null;
volumeSeriesRef.current = null;
};
}, []);
// Handle Chart Type Switching
useEffect(() => {
if (!chartApiRef.current) return;
// Remove existing series safely
if (seriesRef.current) {
try {
chartApiRef.current.removeSeries(seriesRef.current);
} catch (e) {
console.warn('Failed to remove series:', e);
}
seriesRef.current = null;
}
let newSeries;
const commonOptions = {
priceFormat: { type: 'price', precision: 5, minMove: 0.00001 },
};
// Create new series based on type
try {
switch (chartType) {
case 'Line':
newSeries = chartApiRef.current.addLineSeries({
...commonOptions,
color: '#3b82f6',
lineWidth: 2,
});
break;
case 'Area':
newSeries = chartApiRef.current.addAreaSeries({
...commonOptions,
topColor: 'rgba(59, 130, 246, 0.56)',
bottomColor: 'rgba(59, 130, 246, 0.04)',
lineColor: '#3b82f6',
lineWidth: 2,
});
break;
case 'Bar':
newSeries = chartApiRef.current.addBarSeries({
...commonOptions,
upColor: '#22c55e',
downColor: '#ef4444',
});
break;
case 'Candle':
default:
newSeries = chartApiRef.current.addCandlestickSeries({
...commonOptions,
upColor: '#22c55e',
downColor: '#ef4444',
borderUpColor: '#22c55e',
borderDownColor: '#ef4444',
wickUpColor: '#22c55e',
wickDownColor: '#ef4444',
});
break;
}
seriesRef.current = newSeries;
} catch (e) {
console.error('Failed to create series:', e);
}
}, [chartType]);
// Handle Data Updates
useEffect(() => {
if (!seriesRef.current || !volumeSeriesRef.current) return;
try {
const visibleOHLC = data.slice(0, currentIndex + 1);
const visibleVolume = volumeData.slice(0, currentIndex + 1);
if (chartType === 'Line' || chartType === 'Area') {
const lineData = visibleOHLC.map(d => ({ time: d.time, value: d.close }));
seriesRef.current.setData(lineData);
} else {
seriesRef.current.setData(visibleOHLC);
}
volumeSeriesRef.current.setData(visibleVolume);
if (visibleOHLC.length > 0) {
setCandleInfo(visibleOHLC[visibleOHLC.length - 1]);
}
} catch (e) {
console.error('Failed to update data:', e);
}
}, [currentIndex, data, volumeData, chartType]);
return (
<div ref={wrapperRef} className="flex-1 flex flex-col relative overflow-hidden">
{/* Main Chart */}
<div className="flex-1 relative min-h-0">
<div ref={chartContainerRef} className="absolute inset-0" />
{/* Price Info Overlay */}
{candleInfo && (
<div className="absolute top-3 left-3 flex items-center gap-4 z-10 pointer-events-none">
<div className="flex items-center">
<span className="text-xs text-zinc-500 mr-1">O</span>
<span className="text-xs mono text-zinc-300">{candleInfo.open.toFixed(5)}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-zinc-500 mr-1">H</span>
<span className="text-xs mono text-emerald-400">{candleInfo.high.toFixed(5)}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-zinc-500 mr-1">L</span>
<span className="text-xs mono text-red-400">{candleInfo.low.toFixed(5)}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-zinc-500 mr-1">C</span>
<span className="text-xs mono text-zinc-300">{candleInfo.close.toFixed(5)}</span>
</div>
</div>
)}
{/* Status Overlay */}
<div className="absolute top-3 right-3 flex items-center gap-2 px-2 py-1 bg-zinc-900/80 border border-zinc-800 rounded z-10">
<span className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></span>
<span className="text-xs mono text-zinc-300">
{candleInfo ? new Date(candleInfo.time * 1000).toISOString().slice(0, 16).replace('T', ' ') : 'Loading...'}
</span>
</div>
<div className="absolute bottom-3 left-3 flex items-center gap-2 px-2 py-1 bg-zinc-900/80 border border-zinc-800 rounded z-10">
<span className="text-xs text-zinc-300 capitalize">{currentTool}</span>
</div>
</div>
{/* Volume Chart */}
<div className="h-16 border-t border-zinc-800/50 relative bg-zinc-950 flex-shrink-0">
<div ref={volumeContainerRef} className="h-full w-full" />
</div>
</div>
);
});
export default Chart;
+306
View File
@@ -0,0 +1,306 @@
import React, { useState } from 'react';
import {
X, Calendar as CalendarIcon, ChevronLeft, ChevronRight,
TrendingUp, TrendingDown, BookOpen, Smile, Frown, Meh,
AlertCircle, DollarSign, Percent, Hash, MoreHorizontal,
Plus, Filter
} from 'lucide-react';
import { JournalDay, TradeEntry } from '../types';
interface JournalModalProps {
isOpen: boolean;
onClose: () => void;
}
// Mock Data Generator
const getMockData = (dateStr: string): JournalDay | null => {
// Only return data for a few specific dates in Feb 2024
if (dateStr === '2024-02-15') {
return {
date: '2024-02-15',
mood: 'disciplined',
note: "Excellent execution today. Waited for the London open liquidity sweep before entering. Managed risk well on the second trade.",
trades: [
{ id: '1', pair: 'EURUSD', type: 'LONG', time: '09:30', entry: 1.08200, exit: 1.08550, lots: 2.0, pnl: 700, r: 3.5, status: 'WIN', setup: 'London Breakout' },
{ id: '2', pair: 'GBPUSD', type: 'SHORT', time: '11:15', entry: 1.26500, exit: 1.26600, lots: 1.5, pnl: -150, r: -1.0, status: 'LOSS', setup: 'Supply Rejection' },
{ id: '3', pair: 'XAUUSD', type: 'LONG', time: '14:00', entry: 2005.50, exit: 2008.00, lots: 0.5, pnl: 125, r: 1.2, status: 'WIN', setup: 'Support Bounce' }
]
};
}
if (dateStr === '2024-02-14') {
return {
date: '2024-02-14',
mood: 'tilted',
note: "CPI data caused slippage. Revenge traded the aftermath. Need to stay away during high impact news.",
trades: [
{ id: '4', pair: 'EURUSD', type: 'SHORT', time: '08:30', entry: 1.07800, exit: 1.07950, lots: 3.0, pnl: -450, r: -1.0, status: 'LOSS', setup: 'News Fade' },
{ id: '5', pair: 'EURUSD', type: 'LONG', time: '08:45', entry: 1.07950, exit: 1.07850, lots: 5.0, pnl: -500, r: -2.0, status: 'LOSS', setup: 'Revenge' }
]
};
}
return null;
};
const JournalModal: React.FC<JournalModalProps> = ({ isOpen, onClose }) => {
const [selectedDate, setSelectedDate] = useState<string>('2024-02-15');
const [viewMonth, setViewMonth] = useState(new Date('2024-02-01'));
if (!isOpen) return null;
const journalData = getMockData(selectedDate);
// Stats calc
const totalPnL = journalData?.trades.reduce((acc, t) => acc + t.pnl, 0) || 0;
const winRate = journalData ? Math.round((journalData.trades.filter(t => t.status === 'WIN').length / journalData.trades.length) * 100) : 0;
const totalR = journalData?.trades.reduce((acc, t) => acc + t.r, 0) || 0;
// Calendar Grid Gen
const daysInMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 0).getDate();
const startDay = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), 1).getDay(); // 0 is Sun
const calendarDays = [];
// Pad empty days
for (let i = 0; i < (startDay === 0 ? 6 : startDay - 1); i++) {
calendarDays.push(null);
}
// Fill days
for (let i = 1; i <= daysInMonth; i++) {
calendarDays.push(i);
}
const MoodIcon = ({ mood }: { mood: string }) => {
switch(mood) {
case 'disciplined': return <Smile className="text-emerald-400" size={16} />;
case 'tilted': return <AlertCircle className="text-red-400" size={16} />;
case 'neutral': return <Meh className="text-zinc-400" size={16} />;
default: return <Smile className="text-zinc-400" size={16} />;
}
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
<div className="absolute inset-0 bg-black/80 animate-fadeIn backdrop-blur-sm" onClick={onClose} />
<div className="relative w-full max-w-7xl h-[85vh] bg-zinc-950 border border-zinc-800 rounded-xl flex overflow-hidden animate-slideUp shadow-2xl">
{/* Left Panel: Calendar & Nav */}
<div className="w-80 border-r border-zinc-800 flex flex-col bg-zinc-900/50">
<div className="p-4 border-b border-zinc-800 flex items-center justify-between">
<div className="flex items-center gap-2 text-white font-medium">
<BookOpen size={18} className="text-blue-500" />
Trading Journal
</div>
<div className="flex gap-1">
<button className="p-1 hover:bg-zinc-800 rounded"><ChevronLeft size={16} className="text-zinc-400" /></button>
<button className="p-1 hover:bg-zinc-800 rounded"><ChevronRight size={16} className="text-zinc-400" /></button>
</div>
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-white mb-4">February 2024</h3>
<div className="grid grid-cols-7 gap-2 mb-2">
{['M','T','W','T','F','S','S'].map((d, i) => (
<div key={i} className="text-xs text-zinc-500 text-center font-medium">{d}</div>
))}
</div>
<div className="grid grid-cols-7 gap-2">
{calendarDays.map((day, i) => {
if (!day) return <div key={`empty-${i}`} />;
const dateStr = `2024-02-${day.toString().padStart(2, '0')}`;
const data = getMockData(dateStr);
const isSelected = dateStr === selectedDate;
let bgClass = 'bg-zinc-900 border-zinc-800 hover:border-zinc-600';
if (isSelected) bgClass = 'bg-blue-600 border-blue-500 text-white shadow-lg shadow-blue-900/20';
else if (data) {
const pnl = data.trades.reduce((acc, t) => acc + t.pnl, 0);
if (pnl > 0) bgClass = 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400';
else if (pnl < 0) bgClass = 'bg-red-500/10 border-red-500/30 text-red-400';
}
return (
<button
key={i}
onClick={() => setSelectedDate(dateStr)}
className={`aspect-square rounded-lg border flex flex-col items-center justify-center relative transition-all ${bgClass}`}
>
<span className="text-xs font-medium">{day}</span>
{data && (
<div className="flex gap-0.5 mt-1">
<div className={`w-1 h-1 rounded-full ${data.trades.reduce((acc,t)=>acc+t.pnl,0) > 0 ? 'bg-emerald-400' : 'bg-red-400'}`}></div>
</div>
)}
</button>
);
})}
</div>
</div>
<div className="mt-auto p-4 border-t border-zinc-800">
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-zinc-900 rounded-lg border border-zinc-800">
<span className="text-xs text-zinc-500 block mb-1">Monthly P&L</span>
<span className="text-sm font-mono text-emerald-400">+$2,450.00</span>
</div>
<div className="p-3 bg-zinc-900 rounded-lg border border-zinc-800">
<span className="text-xs text-zinc-500 block mb-1">Win Rate</span>
<span className="text-sm font-mono text-blue-400">68%</span>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col bg-zinc-950">
{/* Date Header */}
<div className="h-16 border-b border-zinc-800 flex items-center justify-between px-6 bg-zinc-900/30">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-zinc-800 flex items-center justify-center text-zinc-400">
<CalendarIcon size={20} />
</div>
<div>
<h2 className="text-lg font-semibold text-white">
{new Date(selectedDate).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</h2>
<div className="flex items-center gap-2 text-xs text-zinc-500">
<span className={totalPnL >= 0 ? "text-emerald-400" : "text-red-400"}>
{totalPnL >= 0 ? "Profitable Day" : "Loss Day"}
</span>
<span></span>
<span>{journalData?.trades.length || 0} Trades Taken</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={onClose} className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors">
<X size={20} />
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6">
{/* Daily Stats Row */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-xl">
<div className="flex items-center gap-2 text-zinc-400 mb-2">
<DollarSign size={14} />
<span className="text-xs font-medium uppercase">Net P&L</span>
</div>
<span className={`text-2xl font-mono font-medium ${totalPnL >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{totalPnL >= 0 ? '+' : ''}{totalPnL.toFixed(2)}
</span>
</div>
<div className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-xl">
<div className="flex items-center gap-2 text-zinc-400 mb-2">
<TrendingUp size={14} />
<span className="text-xs font-medium uppercase">R-Multiple</span>
</div>
<span className="text-2xl font-mono font-medium text-white">
{totalR > 0 ? '+' : ''}{totalR.toFixed(1)}R
</span>
</div>
<div className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-xl">
<div className="flex items-center gap-2 text-zinc-400 mb-2">
<Percent size={14} />
<span className="text-xs font-medium uppercase">Win Rate</span>
</div>
<span className="text-2xl font-mono font-medium text-blue-400">
{winRate}%
</span>
</div>
<div className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-xl">
<div className="flex items-center gap-2 text-zinc-400 mb-2">
<Smile size={14} />
<span className="text-xs font-medium uppercase">Psychology</span>
</div>
<div className="flex items-center gap-2">
<span className="text-lg text-white capitalize">{journalData?.mood || 'No Data'}</span>
{journalData && <MoodIcon mood={journalData.mood} />}
</div>
</div>
</div>
{/* Daily Note */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-zinc-300">Daily Reflection</h3>
<button className="text-xs text-blue-400 hover:text-blue-300">Edit</button>
</div>
<div className="w-full p-4 bg-zinc-900/30 border border-zinc-800 rounded-xl text-sm text-zinc-300 leading-relaxed">
{journalData?.note || "No notes recorded for this day."}
</div>
</div>
{/* Trades Table */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-zinc-300">Executed Trades</h3>
<div className="flex gap-2">
<button className="p-1.5 text-zinc-400 hover:text-white bg-zinc-900 border border-zinc-800 rounded"><Filter size={14} /></button>
<button className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-500 rounded transition-colors">
<Plus size={14} /> Add Trade
</button>
</div>
</div>
{journalData && journalData.trades.length > 0 ? (
<div className="border border-zinc-800 rounded-xl overflow-hidden bg-zinc-900/30">
<table className="w-full text-left text-sm">
<thead className="bg-zinc-900/50 text-zinc-500 text-xs uppercase font-medium">
<tr>
<th className="px-4 py-3">Time</th>
<th className="px-4 py-3">Pair</th>
<th className="px-4 py-3">Type</th>
<th className="px-4 py-3">Setup</th>
<th className="px-4 py-3 text-right">Lots</th>
<th className="px-4 py-3 text-right">Entry</th>
<th className="px-4 py-3 text-right">Exit</th>
<th className="px-4 py-3 text-right">R</th>
<th className="px-4 py-3 text-right">P&L</th>
<th className="px-4 py-3"></th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/50">
{journalData.trades.map((trade) => (
<tr key={trade.id} className="hover:bg-zinc-800/30 transition-colors group">
<td className="px-4 py-3 font-mono text-zinc-400">{trade.time}</td>
<td className="px-4 py-3 font-semibold text-white">{trade.pair}</td>
<td className="px-4 py-3">
<span className={`px-1.5 py-0.5 text-[10px] font-bold rounded ${trade.type === 'LONG' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
{trade.type}
</span>
</td>
<td className="px-4 py-3 text-zinc-300">{trade.setup}</td>
<td className="px-4 py-3 text-right font-mono text-zinc-400">{trade.lots.toFixed(2)}</td>
<td className="px-4 py-3 text-right font-mono text-zinc-400">{trade.entry}</td>
<td className="px-4 py-3 text-right font-mono text-zinc-400">{trade.exit}</td>
<td className="px-4 py-3 text-right font-mono">
<span className={trade.r > 0 ? "text-emerald-400" : "text-red-400"}>{trade.r}R</span>
</td>
<td className={`px-4 py-3 text-right font-mono font-medium ${trade.pnl > 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{trade.pnl > 0 ? '+' : ''}{trade.pnl.toFixed(2)}
</td>
<td className="px-4 py-3 text-right">
<button className="text-zinc-600 hover:text-white opacity-0 group-hover:opacity-100 transition-opacity">
<MoreHorizontal size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="h-40 flex flex-col items-center justify-center border border-zinc-800 border-dashed rounded-xl text-zinc-500">
<BookOpen size={24} className="mb-2 opacity-50" />
<span className="text-sm">No trades recorded for this day.</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default JournalModal;
+207
View File
@@ -0,0 +1,207 @@
import React, { useState } from 'react';
import { Brain, Cpu, Database, Play, BarChart, CheckCircle2 } from 'lucide-react';
import { MLConfig } from '../types';
const MachineLearningPanel: React.FC = () => {
const [config, setConfig] = useState<MLConfig>({
modelType: 'LSTM',
features: ['RSI', 'MACD', 'Close'],
epochs: 50,
learningRate: 0.001,
status: 'Idle',
accuracy: 0
});
const [logs, setLogs] = useState<string[]>([]);
const [progress, setProgress] = useState(0);
const toggleFeature = (feature: string) => {
if (config.features.includes(feature)) {
setConfig({ ...config, features: config.features.filter(f => f !== feature) });
} else {
setConfig({ ...config, features: [...config.features, feature] });
}
};
const startTraining = () => {
setConfig({ ...config, status: 'Training', accuracy: 0 });
setLogs(['Initializing TensorFlow backend...', 'Normalizing dataset...', `Building ${config.modelType} architecture...`]);
setProgress(0);
let epoch = 0;
const interval = setInterval(() => {
epoch++;
const currentProgress = (epoch / config.epochs) * 100;
setProgress(currentProgress);
// Log updates
if (epoch % 10 === 0) {
const loss = (Math.random() * 0.5).toFixed(4);
const acc = (0.5 + (epoch/config.epochs) * 0.35).toFixed(4);
setLogs(prev => [`Epoch ${epoch}/${config.epochs}: loss=${loss} - accuracy=${acc}`, ...prev]);
}
if (epoch >= config.epochs) {
clearInterval(interval);
setConfig({ ...config, status: 'Ready', accuracy: 0.85 + Math.random() * 0.1 });
setLogs(prev => ['Model successfully trained and saved.', ...prev]);
}
}, 100);
};
return (
<div className="flex flex-col h-full bg-zinc-900/30 p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Brain className="text-sky-500" />
AI Neural Lab
</h2>
<p className="text-sm text-zinc-500">Train Deep Learning models on historical price action.</p>
</div>
<div className="flex items-center gap-4">
{config.status === 'Ready' && (
<div className="flex items-center gap-2 px-3 py-1 bg-emerald-500/10 border border-emerald-500/20 rounded-full text-emerald-400 text-xs font-medium">
<CheckCircle2 size={14} /> Model Ready ({ (config.accuracy * 100).toFixed(1) }%)
</div>
)}
<button
onClick={startTraining}
disabled={config.status === 'Training'}
className={`px-4 py-2 rounded-lg flex items-center gap-2 font-medium transition-all ${
config.status === 'Training'
? 'bg-zinc-800 text-zinc-500 cursor-not-allowed'
: 'bg-blue-700 hover:bg-blue-600 text-white shadow-lg shadow-blue-900/20'
}`}
>
{config.status === 'Training' ? <Cpu className="animate-pulse" size={18} /> : <Play size={18} fill="currentColor" />}
{config.status === 'Training' ? 'Training Model...' : 'Train Model'}
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-full min-h-0">
{/* Configuration Column */}
<div className="space-y-4">
{/* Model Architecture */}
<div className="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4">
<h3 className="text-xs font-semibold text-zinc-500 uppercase mb-3 flex items-center gap-2">
<Cpu size={14} /> Architecture
</h3>
<div className="space-y-3">
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Model Type</label>
<select
value={config.modelType}
onChange={(e) => setConfig({ ...config, modelType: e.target.value as any })}
className="w-full bg-zinc-800 border border-zinc-700 rounded p-2 text-sm text-white focus:outline-none"
>
<option value="LSTM">Long Short-Term Memory (LSTM)</option>
<option value="Transformer">Transformer (Attention)</option>
<option value="RandomForest">Random Forest Regressor</option>
</select>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Epochs</label>
<input
type="number"
value={config.epochs}
onChange={(e) => setConfig({ ...config, epochs: Number(e.target.value) })}
className="w-full bg-zinc-800 border border-zinc-700 rounded p-2 text-sm text-white"
/>
</div>
<div>
<label className="text-xs text-zinc-400 mb-1.5 block">Learning Rate</label>
<input
type="number"
value={config.learningRate}
step="0.001"
onChange={(e) => setConfig({ ...config, learningRate: Number(e.target.value) })}
className="w-full bg-zinc-800 border border-zinc-700 rounded p-2 text-sm text-white"
/>
</div>
</div>
</div>
</div>
{/* Feature Engineering */}
<div className="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4 flex-1">
<h3 className="text-xs font-semibold text-zinc-500 uppercase mb-3 flex items-center gap-2">
<Database size={14} /> Feature Selection
</h3>
<div className="grid grid-cols-2 gap-2">
{['Open', 'High', 'Low', 'Close', 'Volume', 'RSI', 'MACD', 'EMA 20', 'EMA 50', 'ATR', 'Bollinger', 'Stoch'].map(f => (
<button
key={f}
onClick={() => toggleFeature(f)}
className={`px-3 py-2 rounded text-xs text-left transition-colors ${
config.features.includes(f)
? 'bg-blue-500/20 text-blue-300 border border-blue-500/30'
: 'bg-zinc-800/50 text-zinc-400 border border-transparent hover:bg-zinc-800'
}`}
>
{f}
</button>
))}
</div>
</div>
</div>
{/* Training Visualization (Placeholder for Graph) */}
<div className="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4 flex flex-col relative overflow-hidden">
<h3 className="text-xs font-semibold text-zinc-500 uppercase mb-3 flex items-center gap-2">
<BarChart size={14} /> Loss / Accuracy
</h3>
{config.status === 'Idle' ? (
<div className="flex-1 flex flex-col items-center justify-center text-zinc-600">
<Brain size={48} className="mb-4 opacity-20" />
<p>Configure model and start training</p>
</div>
) : (
<div className="flex-1 flex flex-col justify-end relative">
{/* Simple CSS Bar Graph Simulation */}
<div className="flex items-end gap-1 h-64 w-full">
{Array.from({ length: 40 }).map((_, i) => {
const h = config.status === 'Ready'
? 30 + Math.random() * 60
: (progress > (i*2.5) ? 10 + Math.random() * 50 : 5);
return (
<div
key={i}
className="flex-1 bg-blue-500/50 rounded-t-sm transition-all duration-500"
style={{ height: `${h}%`, opacity: (i/40) }}
></div>
)
})}
</div>
<div className="absolute inset-0 flex items-center justify-center">
{config.status === 'Training' && (
<div className="text-4xl font-mono font-bold text-white/10">{Math.round(progress)}%</div>
)}
</div>
</div>
)}
</div>
{/* Console / Logs */}
<div className="bg-zinc-950 border border-zinc-800 rounded-xl p-4 flex flex-col font-mono text-xs">
<h3 className="text-xs font-semibold text-zinc-500 uppercase mb-3">System Logs</h3>
<div className="flex-1 overflow-y-auto space-y-1 text-zinc-400">
{logs.length === 0 && <span className="text-zinc-600 italic">System ready.</span>}
{logs.map((log, i) => (
<div key={i} className="border-l-2 border-zinc-800 pl-2 py-0.5">
<span className="text-zinc-600 mr-2">[{new Date().toLocaleTimeString()}]</span>
<span className={log.includes('successfully') ? 'text-emerald-400' : ''}>{log}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default MachineLearningPanel;
+253
View File
@@ -0,0 +1,253 @@
import React, { useState } from 'react';
import { Sliders, Play, RotateCw, TrendingUp, Grid, List, Activity } from 'lucide-react';
import { OptimizationParam, OptimizationResult } from '../types';
const OptimizationPanel: React.FC = () => {
const [viewMode, setViewMode] = useState<'table' | 'graph'>('table');
const [params, setParams] = useState<OptimizationParam[]>([
{ id: '1', name: 'RSI Period', min: 7, max: 21, step: 1, current: 14, enabled: true },
{ id: '2', name: 'Stop Loss', min: 10, max: 50, step: 5, current: 20, enabled: true },
{ id: '3', name: 'Take Profit', min: 20, max: 100, step: 10, current: 40, enabled: true },
{ id: '4', name: 'MA Filter', min: 20, max: 200, step: 20, current: 50, enabled: false },
]);
const [results, setResults] = useState<OptimizationResult[]>([]);
const [isOptimizing, setIsOptimizing] = useState(false);
const [progress, setProgress] = useState(0);
const toggleParam = (id: string) => {
setParams(params.map(p => p.id === id ? { ...p, enabled: !p.enabled } : p));
};
const runOptimization = () => {
setIsOptimizing(true);
setResults([]);
setProgress(0);
// Simulate Genetic Algorithm process
let step = 0;
const totalSteps = 20;
const interval = setInterval(() => {
step++;
setProgress((step / totalSteps) * 100);
// Generate mock result
const profit = Math.floor(Math.random() * 5000) - 1000;
const dd = Math.floor(Math.random() * 15);
const score = (profit / (dd + 1)).toFixed(2);
const newResult: OptimizationResult = {
pass: step,
params: `RSI=${7 + Math.floor(Math.random()*14)}, SL=${10 + Math.floor(Math.random()*40)}`,
profit: profit,
drawdown: dd,
score: Number(score)
};
setResults(prev => [newResult, ...prev].sort((a, b) => b.score - a.score));
if (step >= totalSteps) {
clearInterval(interval);
setIsOptimizing(false);
}
}, 150);
};
// Generate Mock Heatmap Data (2D array)
// 10x10 grid simulation
const renderHeatmap = () => {
const gridSize = 12;
const cells = [];
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
// Simulate fitness landscape
// Higher values in the center
const cx = 6, cy = 6;
const dist = Math.sqrt(Math.pow(i - cx, 2) + Math.pow(j - cy, 2));
const intensity = Math.max(0.1, 1 - (dist / 8));
const noise = (Math.random() - 0.5) * 0.2;
const value = Math.max(0, intensity + noise);
// Color mapping: Green (High), Yellow (Med), Red (Low)
let color;
if (value > 0.7) color = `rgba(34, 197, 94, ${value})`; // Green
else if (value > 0.4) color = `rgba(234, 179, 8, ${value})`; // Yellow
else color = `rgba(239, 68, 68, ${value + 0.2})`; // Red
cells.push(
<div
key={`${i}-${j}`}
className="rounded-sm hover:scale-110 transition-transform duration-200 cursor-pointer relative group border border-black/20"
style={{ backgroundColor: color }}
>
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-[10px] px-2 py-1 rounded border border-zinc-700 whitespace-nowrap z-50">
Profit: ${(value * 5000).toFixed(0)}<br/>
Param A: {i * 2 + 10}<br/>
Param B: {j * 5 + 20}
</div>
</div>
);
}
}
return cells;
};
return (
<div className="flex flex-col h-full bg-zinc-900/30 p-6 overflow-hidden">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Sliders className="text-blue-500" />
Genetic Optimization
</h2>
<p className="text-sm text-zinc-500">Find robust parameters using evolutionary algorithms.</p>
</div>
<button
onClick={runOptimization}
disabled={isOptimizing}
className={`px-4 py-2 rounded-lg flex items-center gap-2 font-medium transition-all ${
isOptimizing
? 'bg-zinc-800 text-zinc-500 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-900/20'
}`}
>
{isOptimizing ? <RotateCw className="animate-spin" size={18} /> : <Play size={18} fill="currentColor" />}
{isOptimizing ? 'Optimizing...' : 'Start Optimization'}
</button>
</div>
<div className="flex gap-6 h-full min-h-0">
{/* Parameters Settings */}
<div className="w-1/3 bg-zinc-900/50 border border-zinc-800 rounded-xl p-4 flex flex-col">
<h3 className="text-xs font-semibold text-zinc-500 uppercase mb-4">Parameter Configuration</h3>
<div className="space-y-3 overflow-y-auto pr-2">
{params.map(p => (
<div key={p.id} className={`p-3 border rounded-lg transition-colors ${p.enabled ? 'bg-zinc-800/50 border-zinc-700' : 'bg-zinc-900/30 border-zinc-800 opacity-60'}`}>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={p.enabled}
onChange={() => toggleParam(p.id)}
className="rounded border-zinc-600 bg-zinc-700 text-blue-500 focus:ring-0"
/>
<span className="text-sm font-medium text-white">{p.name}</span>
</div>
<span className="text-xs mono text-zinc-400">Current: {p.current}</span>
</div>
{p.enabled && (
<div className="grid grid-cols-3 gap-2">
<div>
<label className="text-[10px] text-zinc-500 block">Start</label>
<input type="number" value={p.min} className="w-full bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs text-white" readOnly />
</div>
<div>
<label className="text-[10px] text-zinc-500 block">Step</label>
<input type="number" value={p.step} className="w-full bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs text-white" readOnly />
</div>
<div>
<label className="text-[10px] text-zinc-500 block">Stop</label>
<input type="number" value={p.max} className="w-full bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs text-white" readOnly />
</div>
</div>
)}
</div>
))}
</div>
</div>
{/* Results Panel */}
<div className="flex-1 bg-zinc-900/50 border border-zinc-800 rounded-xl flex flex-col overflow-hidden">
<div className="p-4 border-b border-zinc-800 flex justify-between items-center">
<div className="flex items-center gap-4">
<h3 className="text-xs font-semibold text-zinc-500 uppercase">Optimization Results</h3>
<div className="flex items-center bg-zinc-900 border border-zinc-800 rounded p-0.5">
<button
onClick={() => setViewMode('table')}
className={`p-1.5 rounded transition-colors ${viewMode === 'table' ? 'bg-zinc-800 text-white' : 'text-zinc-500 hover:text-zinc-300'}`}
title="Table View"
>
<List size={14} />
</button>
<button
onClick={() => setViewMode('graph')}
className={`p-1.5 rounded transition-colors ${viewMode === 'graph' ? 'bg-zinc-800 text-white' : 'text-zinc-500 hover:text-zinc-300'}`}
title="Graph View"
>
<Grid size={14} />
</button>
</div>
</div>
{isOptimizing && (
<div className="w-48 h-2 bg-zinc-800 rounded-full overflow-hidden">
<div className="h-full bg-blue-500 transition-all duration-300" style={{ width: `${progress}%` }}></div>
</div>
)}
</div>
<div className="flex-1 overflow-hidden relative">
{viewMode === 'table' ? (
<div className="absolute inset-0 overflow-y-auto">
<table className="w-full text-left text-xs">
<thead className="bg-zinc-900/80 text-zinc-500 uppercase font-medium sticky top-0 z-10">
<tr>
<th className="px-4 py-3">Pass</th>
<th className="px-4 py-3">Parameters</th>
<th className="px-4 py-3 text-right">Profit</th>
<th className="px-4 py-3 text-right">Drawdown</th>
<th className="px-4 py-3 text-right">Score</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/50">
{results.length === 0 && !isOptimizing && (
<tr>
<td colSpan={5} className="px-4 py-12 text-center text-zinc-500 italic">
Start optimization to see results...
</td>
</tr>
)}
{results.map((r, i) => (
<tr key={i} className={`hover:bg-zinc-800/30 transition-colors ${i === 0 ? 'bg-emerald-500/5' : ''}`}>
<td className="px-4 py-2 font-mono text-zinc-500">#{r.pass}</td>
<td className="px-4 py-2 text-zinc-300">{r.params}</td>
<td className={`px-4 py-2 text-right font-mono font-medium ${r.profit > 0 ? 'text-emerald-400' : 'text-red-400'}`}>
${r.profit.toLocaleString()}
</td>
<td className="px-4 py-2 text-right font-mono text-red-300">{r.drawdown}%</td>
<td className="px-4 py-2 text-right font-bold text-blue-400">{r.score}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="absolute inset-0 flex flex-col p-6">
{/* Graph View Simulation */}
<div className="flex items-center justify-between mb-4">
<div className="text-xs text-zinc-400">
X: <span className="text-white">RSI Period (7-21)</span>
</div>
<div className="text-xs text-zinc-400">
Y: <span className="text-white">Stop Loss (10-50)</span>
</div>
</div>
<div className="flex-1 grid grid-cols-12 grid-rows-12 gap-1 bg-zinc-900/50 p-1 border border-zinc-800 rounded-lg">
{renderHeatmap()}
</div>
<div className="flex items-center justify-center gap-4 mt-4 text-[10px] text-zinc-500 uppercase font-medium">
<div className="flex items-center gap-1"><div className="w-3 h-3 bg-red-500/80 rounded-sm"></div> Low Profit</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 bg-yellow-500/80 rounded-sm"></div> Med Profit</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 bg-emerald-500/80 rounded-sm"></div> High Profit</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default OptimizationPanel;
+216
View File
@@ -0,0 +1,216 @@
import React, { useEffect, useRef } from 'react';
import { createChart, ColorType } from 'lightweight-charts';
import { BacktestResult } from '../types';
import { TrendingUp, Activity, BarChart3, AlertTriangle, ShieldCheck } from 'lucide-react';
interface QuantumAnalyticsProps {
results: BacktestResult;
}
const QuantumAnalytics: React.FC<QuantumAnalyticsProps> = ({ results }) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chartContainerRef.current) return;
const chart = createChart(chartContainerRef.current, {
layout: {
background: { type: ColorType.Solid, color: 'transparent' },
textColor: '#71717a',
fontFamily: "'Inter', sans-serif",
},
grid: {
vertLines: { visible: false },
horzLines: { color: '#1f1f23' }
},
width: chartContainerRef.current.clientWidth,
height: 250,
rightPriceScale: { borderColor: '#27272a' },
timeScale: { borderColor: '#27272a', timeVisible: true },
});
const areaSeries = chart.addAreaSeries({
topColor: 'rgba(59, 130, 246, 0.4)', // Corporate Blue
bottomColor: 'rgba(59, 130, 246, 0.05)',
lineColor: '#3b82f6',
lineWidth: 2,
});
areaSeries.setData(results.equityCurve);
const resizeObserver = new ResizeObserver(() => {
if (chartContainerRef.current) {
chart.applyOptions({ width: chartContainerRef.current.clientWidth });
}
});
resizeObserver.observe(chartContainerRef.current);
return () => {
resizeObserver.disconnect();
chart.remove();
};
}, [results]);
const TableRow = ({ label, val1, val2, label2, isHeader = false }: any) => (
<div className={`grid grid-cols-4 py-2 border-b border-zinc-800/50 ${isHeader ? 'text-zinc-500 font-semibold' : 'text-zinc-300 hover:bg-zinc-800/20'}`}>
<div className="col-span-1 pl-4 text-xs flex items-center">{label}</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">{val1}</div>
{label2 ? (
<>
<div className="col-span-1 pl-4 text-xs flex items-center">{label2}</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">{val2}</div>
</>
) : (
<div className="col-span-2"></div>
)}
</div>
);
const StatValue = ({ val, colorClass }: { val: string | number, colorClass?: string }) => (
<span className={colorClass || 'text-zinc-200'}>{val}</span>
);
return (
<div className="h-full flex flex-col bg-zinc-950/50 overflow-y-auto animate-fadeIn">
{/* Header / Summary Bar */}
<div className="flex flex-col border-b border-zinc-800 bg-zinc-900/50">
<div className="px-6 py-4 flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Activity className="text-blue-500" />
Backtest Report
</h2>
<div className="flex items-center gap-4 mt-1">
<span className="text-xs text-zinc-500">Ticks modelled: <span className="text-zinc-300">{results.ticksModelled.toLocaleString()}</span></span>
<span className="text-xs text-zinc-500">Errors: <span className="text-zinc-300">0</span></span>
</div>
</div>
<div className="w-1/3">
<div className="flex justify-between text-[10px] uppercase text-zinc-500 mb-1">
<span>Modelling Quality</span>
<span className="text-emerald-500">{results.modellingQuality}%</span>
</div>
<div className="h-2 w-full bg-zinc-800 rounded-full overflow-hidden">
<div className="h-full bg-zinc-600 w-[10%] inline-block"></div>
<div className="h-full bg-emerald-500 w-[90%] inline-block"></div>
</div>
</div>
</div>
</div>
<div className="p-6 space-y-6">
{/* Main Stats Table - Mimicking MT5 Layout */}
<div className="border border-zinc-800 rounded-xl bg-zinc-900/30 overflow-hidden">
<TableRow
label="Initial Deposit"
val1={<StatValue val={results.initialDeposit.toFixed(2)} />}
label2="Spread"
val2={<StatValue val="10" />}
/>
<TableRow
label="Total Net Profit"
val1={<StatValue val={results.netProfit.toFixed(2)} colorClass={results.netProfit >= 0 ? 'text-emerald-400 font-bold' : 'text-red-400 font-bold'} />}
label2="Gross Profit"
val2={<StatValue val={results.grossProfit.toFixed(2)} />}
/>
<div className="grid grid-cols-4 py-2 border-b border-zinc-800/50 text-zinc-300 hover:bg-zinc-800/20">
<div className="col-span-1 pl-4 text-xs flex items-center">Gross Loss</div>
<div className="col-span-1 text-right text-sm font-mono pr-4 text-red-400">{results.grossLoss.toFixed(2)}</div>
<div className="col-span-1 pl-4 text-xs flex items-center">Expected Payoff</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">{results.expectedPayoff.toFixed(2)}</div>
</div>
<TableRow
label="Profit Factor"
val1={<StatValue val={results.profitFactor.toFixed(2)} />}
label2="Sharpe Ratio"
val2={<StatValue val={results.sharpeRatio.toFixed(2)} />}
/>
<div className="grid grid-cols-4 py-2 border-b border-zinc-800/50 text-zinc-300 hover:bg-zinc-800/20">
<div className="col-span-1 pl-4 text-xs flex items-center">Absolute Drawdown</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">{results.absoluteDrawdown.toFixed(2)}</div>
<div className="col-span-1 pl-4 text-xs flex items-center">Maximal Drawdown</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">
{results.maxDrawdown.toFixed(2)} <span className="text-zinc-500 text-xs">({results.maxDrawdownPercent}%)</span>
</div>
</div>
<div className="h-2 bg-zinc-900/50 border-b border-zinc-800/50"></div>
<div className="grid grid-cols-4 py-2 border-b border-zinc-800/50 text-zinc-300 hover:bg-zinc-800/20">
<div className="col-span-1 pl-4 text-xs flex items-center">Total Trades</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">{results.totalTrades}</div>
<div className="col-span-2"></div>
</div>
<div className="grid grid-cols-4 py-2 border-b border-zinc-800/50 text-zinc-300 hover:bg-zinc-800/20">
<div className="col-span-1 pl-4 text-xs flex items-center">Short Positions (won %)</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">
{results.shortPositions} <span className="text-zinc-500 text-xs">({((results.shortWon/results.shortPositions)*100).toFixed(2)}%)</span>
</div>
<div className="col-span-1 pl-4 text-xs flex items-center">Long Positions (won %)</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">
{results.longPositions} <span className="text-zinc-500 text-xs">({((results.longWon/results.longPositions)*100).toFixed(2)}%)</span>
</div>
</div>
<div className="grid grid-cols-4 py-2 border-b border-zinc-800/50 text-zinc-300 hover:bg-zinc-800/20">
<div className="col-span-1 pl-4 text-xs flex items-center">Profit Trades (% of total)</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">
{results.profitTrades} <span className="text-zinc-500 text-xs">({((results.profitTrades/results.totalTrades)*100).toFixed(2)}%)</span>
</div>
<div className="col-span-1 pl-4 text-xs flex items-center">Loss Trades (% of total)</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">
{results.lossTrades} <span className="text-zinc-500 text-xs">({((results.lossTrades/results.totalTrades)*100).toFixed(2)}%)</span>
</div>
</div>
<div className="h-2 bg-zinc-900/50 border-b border-zinc-800/50"></div>
<TableRow
label="Largest Profit Trade"
val1={<StatValue val={results.largestProfitTrade.toFixed(2)} colorClass="text-emerald-400" />}
label2="Largest Loss Trade"
val2={<StatValue val={results.largestLossTrade.toFixed(2)} colorClass="text-red-400" />}
/>
<TableRow
label="Average Profit Trade"
val1={<StatValue val={results.averageProfitTrade.toFixed(2)} />}
label2="Average Loss Trade"
val2={<StatValue val={results.averageLossTrade.toFixed(2)} />}
/>
<div className="h-2 bg-zinc-900/50 border-b border-zinc-800/50"></div>
<div className="grid grid-cols-4 py-2 border-b border-zinc-800/50 text-zinc-300 hover:bg-zinc-800/20">
<div className="col-span-1 pl-4 text-xs flex items-center">Max Consecutive Wins ($)</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">
{results.maxConsecutiveWins} <span className="text-zinc-500 text-xs">({results.maxConsecutiveWinsValue.toFixed(2)})</span>
</div>
<div className="col-span-1 pl-4 text-xs flex items-center">Max Consecutive Losses ($)</div>
<div className="col-span-1 text-right text-sm font-mono pr-4">
{results.maxConsecutiveLosses} <span className="text-zinc-500 text-xs">({results.maxConsecutiveLossesValue.toFixed(2)})</span>
</div>
</div>
</div>
{/* Equity Graph */}
<div className="bg-zinc-900/30 border border-zinc-800 rounded-xl p-4">
<div className="text-xs font-semibold text-zinc-500 uppercase mb-4 pl-2">Balance/Equity Graph</div>
<div ref={chartContainerRef} className="w-full" />
</div>
</div>
</div>
);
};
export default QuantumAnalytics;
+140
View File
@@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { LayoutDashboard, Sliders, Brain, FileText, Zap, Settings, PlayCircle } from 'lucide-react';
import StrategyBuilder from './StrategyBuilder';
import BacktestConfigPanel from './BacktestConfigPanel';
import OptimizationPanel from './OptimizationPanel';
import MachineLearningPanel from './MachineLearningPanel';
import QuantumAnalytics from './QuantumAnalytics';
import { StrategyConfig, BacktestResult, BacktestSettings } from '../types';
interface QuantumLabProps {
strategyConfig: StrategyConfig;
setStrategyConfig: (c: StrategyConfig) => void;
quantumResults: BacktestResult | null;
runBacktest: () => void;
isRunning: boolean;
}
const QuantumLab: React.FC<QuantumLabProps> = ({
strategyConfig,
setStrategyConfig,
quantumResults,
runBacktest,
isRunning
}) => {
// New default tab is 'configure' to mimic MT5 starting point
const [activeTab, setActiveTab] = useState<'compose' | 'configure' | 'optimize' | 'ml' | 'results'>('configure');
const handleRunFromConfig = (settings: BacktestSettings) => {
// In a real app, we would use the 'settings' object to configure the backend run
console.log("Running with settings:", settings);
runBacktest();
setActiveTab('results');
};
return (
<div className="flex flex-col h-full bg-zinc-950">
{/* Lab Navigation */}
<div className="h-12 border-b border-zinc-800 flex items-center px-4 gap-1 bg-zinc-900/50">
<button
onClick={() => setActiveTab('compose')}
className={`px-4 py-2 text-xs font-medium rounded-lg flex items-center gap-2 transition-colors ${activeTab === 'compose' ? 'bg-zinc-800 text-white' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<LayoutDashboard size={14} /> Compose
</button>
<div className="w-px h-4 bg-zinc-800 mx-1"></div>
<button
onClick={() => setActiveTab('configure')}
className={`px-4 py-2 text-xs font-medium rounded-lg flex items-center gap-2 transition-colors ${activeTab === 'configure' ? 'bg-zinc-800 text-white' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<Settings size={14} /> Configure & Run
</button>
<button
onClick={() => setActiveTab('optimize')}
className={`px-4 py-2 text-xs font-medium rounded-lg flex items-center gap-2 transition-colors ${activeTab === 'optimize' ? 'bg-zinc-800 text-white' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<Sliders size={14} /> Optimization
</button>
<button
onClick={() => setActiveTab('ml')}
className={`px-4 py-2 text-xs font-medium rounded-lg flex items-center gap-2 transition-colors ${activeTab === 'ml' ? 'bg-zinc-800 text-white' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<Brain size={14} /> AI Lab
</button>
<div className="w-px h-6 bg-zinc-800 mx-2"></div>
<button
onClick={() => setActiveTab('results')}
className={`px-4 py-2 text-xs font-medium rounded-lg flex items-center gap-2 transition-colors ${activeTab === 'results' ? 'bg-blue-500/10 text-blue-400 border border-blue-500/20' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<FileText size={14} /> Results
</button>
</div>
{/* Lab Content */}
<div className="flex-1 overflow-hidden relative">
{/* COMPOSE TAB (Strategy Builder) */}
{activeTab === 'compose' && (
<div className="absolute inset-0 flex">
<div className="w-80 border-r border-zinc-800 bg-zinc-900/30">
<StrategyBuilder
config={strategyConfig}
setConfig={setStrategyConfig}
onRun={() => setActiveTab('configure')} // Redirect to config instead of immediate run
isRunning={isRunning}
/>
</div>
<div className="flex-1 flex items-center justify-center text-zinc-700 select-none bg-zinc-950/50">
<div className="text-center">
<Zap size={64} className="mx-auto mb-4 opacity-10" />
<p className="text-sm">Logic Node Canvas (Coming Soon)</p>
<p className="text-xs text-zinc-600 mt-2">Visual programming interface for strategy logic.</p>
</div>
</div>
</div>
)}
{/* CONFIGURE TAB (MT5 Style Settings) */}
{activeTab === 'configure' && (
<div className="absolute inset-0">
<BacktestConfigPanel onRun={handleRunFromConfig} isRunning={isRunning} />
</div>
)}
{/* OPTIMIZATION TAB */}
{activeTab === 'optimize' && (
<div className="absolute inset-0">
<OptimizationPanel />
</div>
)}
{/* ML TAB */}
{activeTab === 'ml' && (
<div className="absolute inset-0">
<MachineLearningPanel />
</div>
)}
{/* RESULTS TAB */}
{activeTab === 'results' && (
<div className="absolute inset-0">
{quantumResults ? (
<QuantumAnalytics results={quantumResults} />
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-500">
<FileText size={48} className="mb-4 opacity-20" />
<p>No backtest results available.</p>
<button onClick={() => setActiveTab('configure')} className="mt-4 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-xs transition-colors flex items-center gap-2">
<PlayCircle size={14} /> Go to Configuration
</button>
</div>
)}
</div>
)}
</div>
</div>
);
};
export default QuantumLab;
+145
View File
@@ -0,0 +1,145 @@
import React, { useState } from 'react';
import { X, BookmarkPlus, PencilRuler, Repeat, Lightbulb, GraduationCap, Check } from 'lucide-react';
interface SaveModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: any) => void;
}
const SaveModal: React.FC<SaveModalProps> = ({ isOpen, onClose, onSave }) => {
const [title, setTitle] = useState('');
const [category, setCategory] = useState<'drawing' | 'replay' | 'idea' | 'lesson'>('drawing');
const [tags, setTags] = useState<string[]>([]);
const [notes, setNotes] = useState('');
if (!isOpen) return null;
const availableTags = ['breakout', 'support', 'resistance', 'reversal', 'fibonacci', 'pattern', 'trend'];
const toggleTag = (tag: string) => {
if (tags.includes(tag)) {
setTags(tags.filter(t => t !== tag));
} else {
setTags([...tags, tag]);
}
};
const handleSave = () => {
if (!title) return;
onSave({ title, category, tags, notes });
// Reset form
setTitle('');
setCategory('drawing');
setTags([]);
setNotes('');
};
const CategoryBtn = ({ id, icon: Icon, label }: { id: any, icon: any, label: string }) => (
<button
onClick={() => setCategory(id)}
className={`flex items-center gap-2 px-3 py-2.5 text-xs border rounded-lg transition-colors ${
category === id
? 'bg-blue-500/10 border-blue-500 text-blue-400'
: 'bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:border-zinc-600'
}`}
>
<Icon size={14} />
{label}
</button>
);
return (
<div className="fixed inset-0 z-[110] flex items-center justify-center">
<div className="absolute inset-0 bg-black/70 animate-fadeIn" onClick={onClose} />
<div className="relative w-full max-w-lg bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden animate-slideUp shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-lg">
<BookmarkPlus size={18} className="text-blue-400" />
</div>
<h2 className="text-sm font-semibold text-white">Save to Vault</h2>
</div>
<button onClick={onClose} className="p-1.5 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors">
<X size={16} />
</button>
</div>
{/* Content */}
<div className="p-5 space-y-4">
<div>
<label className="text-xs font-medium text-zinc-400 mb-1.5 block">Title *</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g., EUR/USD Breakout Setup"
className="w-full h-10 px-3 text-sm bg-zinc-800/50 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500 transition-colors"
/>
</div>
<div>
<label className="text-xs font-medium text-zinc-400 mb-1.5 block">Category *</label>
<div className="grid grid-cols-2 gap-2">
<CategoryBtn id="drawing" icon={PencilRuler} label="Drawing Analysis" />
<CategoryBtn id="replay" icon={Repeat} label="Replay Session" />
<CategoryBtn id="idea" icon={Lightbulb} label="Trade Idea" />
<CategoryBtn id="lesson" icon={GraduationCap} label="Lesson Learned" />
</div>
</div>
<div>
<label className="text-xs font-medium text-zinc-400 mb-1.5 block">Tags</label>
<div className="flex flex-wrap gap-1.5">
{availableTags.map(tag => (
<button
key={tag}
onClick={() => toggleTag(tag)}
className={`px-2 py-1 text-xs rounded transition-colors ${
tags.includes(tag)
? 'bg-blue-500/20 text-blue-400'
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700'
}`}
>
{tags.includes(tag) ? '✓ ' : '+ '}{tag}
</button>
))}
</div>
</div>
<div>
<label className="text-xs font-medium text-zinc-400 mb-1.5 block">Notes</label>
<textarea
rows={3}
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Add any observations, reasoning, or lessons..."
className="w-full px-3 py-2 text-sm bg-zinc-800/50 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500 resize-none transition-colors"
/>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between px-5 py-4 border-t border-zinc-800 bg-zinc-900/50">
<button onClick={onClose} className="px-4 py-2 text-xs font-medium text-zinc-400 hover:text-white transition-colors">
Cancel
</button>
<button
onClick={handleSave}
disabled={!title}
className={`flex items-center gap-2 px-4 py-2 text-xs font-medium text-white rounded-lg transition-colors ${
title ? 'bg-blue-600 hover:bg-blue-500' : 'bg-zinc-800 cursor-not-allowed text-zinc-500'
}`}
>
<Check size={14} />
Save to Vault
</button>
</div>
</div>
</div>
);
};
export default SaveModal;
+118
View File
@@ -0,0 +1,118 @@
import React from 'react';
import {
Crosshair, MousePointer2, TrendingUp, Minus, SeparatorVertical,
MoveUpRight, MoveDiagonal, GitBranch, GitMerge, GitFork,
Square, Circle, Triangle, AlignCenterHorizontal, Pentagon,
Mountain, Waves, Type, StickyNote, MessageSquare, ArrowUpRight,
Tag, Ruler, CalendarRange, ArrowBigUp, ArrowBigDown,
Magnet, Lock, Eye, Trash2
} from 'lucide-react';
import { ToolType } from '../types';
interface SidebarProps {
currentTool: ToolType;
setTool: (t: ToolType) => void;
onDeleteAll: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ currentTool, setTool, onDeleteAll }) => {
const ToolBtn = ({ tool, icon: Icon, title }: { tool: ToolType, icon: any, title: string }) => {
const isActive = currentTool === tool;
return (
<button
onClick={() => setTool(tool)}
className={`w-full p-2 rounded flex items-center justify-center transition-all duration-150 ease-in-out ${
isActive
? 'bg-blue-500/15 text-blue-500'
: 'text-zinc-400 hover:bg-zinc-800/50'
}`}
title={title}
>
<Icon size={16} strokeWidth={1.5} />
</button>
);
};
const Separator = () => <div className="w-6 h-px bg-zinc-800 mx-auto my-2"></div>;
return (
<aside className="w-12 border-r border-zinc-800/50 bg-zinc-950/50 flex flex-col py-2 select-none">
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="crosshair" icon={Crosshair} title="Crosshair" />
<ToolBtn tool="pointer" icon={MousePointer2} title="Pointer" />
</div>
<Separator />
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="trendline" icon={TrendingUp} title="Trend Line" />
<ToolBtn tool="hline" icon={Minus} title="Horizontal Line" />
<ToolBtn tool="vline" icon={SeparatorVertical} title="Vertical Line" />
<ToolBtn tool="ray" icon={MoveUpRight} title="Ray" />
<ToolBtn tool="extended" icon={MoveDiagonal} title="Extended Line" />
</div>
<Separator />
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="fibretracement" icon={GitBranch} title="Fib Retracement" />
<ToolBtn tool="fibextension" icon={GitMerge} title="Fib Extension" />
<ToolBtn tool="pitchfork" icon={GitFork} title="Pitchfork" />
</div>
<Separator />
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="rectangle" icon={Square} title="Rectangle" />
<ToolBtn tool="circle" icon={Circle} title="Circle" />
<ToolBtn tool="triangle" icon={Triangle} title="Triangle" />
<ToolBtn tool="channel" icon={AlignCenterHorizontal} title="Parallel Channel" />
</div>
<Separator />
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="xabcd" icon={Pentagon} title="XABCD Pattern" />
<ToolBtn tool="headshoulders" icon={Mountain} title="Head & Shoulders" />
<ToolBtn tool="elliott" icon={Waves} title="Elliott Wave" />
</div>
<Separator />
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="text" icon={Type} title="Text" />
<ToolBtn tool="note" icon={StickyNote} title="Note" />
<ToolBtn tool="callout" icon={MessageSquare} title="Callout" />
<ToolBtn tool="arrow" icon={ArrowUpRight} title="Arrow" />
<ToolBtn tool="pricelabel" icon={Tag} title="Price Label" />
</div>
<Separator />
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="pricerange" icon={Ruler} title="Price Range" />
<ToolBtn tool="daterange" icon={CalendarRange} title="Date Range" />
<ToolBtn tool="longposition" icon={ArrowBigUp} title="Long Position" />
<ToolBtn tool="shortposition" icon={ArrowBigDown} title="Short Position" />
</div>
<div className="flex-1"></div>
<div className="px-1.5 space-y-0.5">
<ToolBtn tool="magnet" icon={Magnet} title="Magnet Mode" />
<ToolBtn tool="lock" icon={Lock} title="Lock Drawings" />
<ToolBtn tool="visibility" icon={Eye} title="Show/Hide Drawings" />
<button
onClick={onDeleteAll}
className="w-full p-2 rounded flex items-center justify-center text-zinc-400 hover:bg-red-500/10 hover:text-red-400 transition-all duration-150"
title="Delete All"
>
<Trash2 size={16} strokeWidth={1.5} />
</button>
</div>
</aside>
);
};
export default Sidebar;
+165
View File
@@ -0,0 +1,165 @@
import React from 'react';
import { Trash2, Plus, Zap, ArrowRight } from 'lucide-react';
import { StrategyConfig, StrategyCondition } from '../types';
interface StrategyBuilderProps {
config: StrategyConfig;
setConfig: (config: StrategyConfig) => void;
onRun: () => void;
isRunning: boolean;
}
const StrategyBuilder: React.FC<StrategyBuilderProps> = ({ config, setConfig, onRun, isRunning }) => {
const addCondition = (type: 'entry' | 'exit') => {
const newCondition: StrategyCondition = {
id: Date.now().toString(),
indicator: 'RSI',
operator: '<',
value: 30,
param1: 14
};
if (type === 'entry') {
setConfig({ ...config, entryConditions: [...config.entryConditions, newCondition] });
} else {
setConfig({ ...config, exitConditions: [...config.exitConditions, newCondition] });
}
};
const removeCondition = (type: 'entry' | 'exit', id: string) => {
if (type === 'entry') {
setConfig({ ...config, entryConditions: config.entryConditions.filter(c => c.id !== id) });
} else {
setConfig({ ...config, exitConditions: config.exitConditions.filter(c => c.id !== id) });
}
};
const updateCondition = (type: 'entry' | 'exit', id: string, field: keyof StrategyCondition, value: any) => {
const list = type === 'entry' ? config.entryConditions : config.exitConditions;
const updatedList = list.map(c => c.id === id ? { ...c, [field]: value } : c);
if (type === 'entry') {
setConfig({ ...config, entryConditions: updatedList });
} else {
setConfig({ ...config, exitConditions: updatedList });
}
};
return (
<div className="flex flex-col h-full bg-zinc-900/50 border-r border-zinc-800 p-4 overflow-y-auto">
<div className="flex items-center gap-2 mb-6">
<div className="p-2 bg-slate-700/50 rounded-lg">
<Zap size={20} className="text-sky-400" />
</div>
<div>
<h2 className="text-sm font-bold text-white uppercase tracking-wider">Quantum Engine</h2>
<p className="text-[10px] text-zinc-500">Logic Builder v1.0</p>
</div>
</div>
{/* Entry Logic */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-semibold text-emerald-400 uppercase">Entry Logic (Buy)</span>
<button onClick={() => addCondition('entry')} className="p-1 hover:bg-zinc-800 rounded text-zinc-400 hover:text-white">
<Plus size={14} />
</button>
</div>
<div className="space-y-2">
{config.entryConditions.map(c => (
<div key={c.id} className="p-3 bg-zinc-800/50 border border-zinc-700 rounded-lg flex items-center gap-2 text-xs">
<select
value={c.indicator}
onChange={(e) => updateCondition('entry', c.id, 'indicator', e.target.value)}
className="bg-transparent text-white font-medium focus:outline-none"
>
<option value="RSI">RSI</option>
<option value="EMA">EMA</option>
<option value="Price">Price</option>
</select>
<select
value={c.operator}
onChange={(e) => updateCondition('entry', c.id, 'operator', e.target.value)}
className="bg-zinc-900 rounded px-1 py-0.5 text-zinc-300 focus:outline-none"
>
<option value="<">&lt;</option>
<option value=">">&gt;</option>
<option value="==">=</option>
<option value="CrossOver">Crosses Over</option>
</select>
<input
type="number"
value={c.value}
onChange={(e) => updateCondition('entry', c.id, 'value', Number(e.target.value))}
className="w-12 bg-zinc-900 rounded px-1 py-0.5 text-white focus:outline-none"
/>
<button onClick={() => removeCondition('entry', c.id)} className="ml-auto text-zinc-500 hover:text-red-400">
<Trash2 size={12} />
</button>
</div>
))}
{config.entryConditions.length === 0 && (
<div className="text-xs text-zinc-600 italic text-center py-2">No entry conditions</div>
)}
</div>
</div>
<div className="w-full h-px bg-zinc-800 my-2"></div>
{/* Exit Logic */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-semibold text-red-400 uppercase">Risk Management</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[10px] text-zinc-500 uppercase block mb-1">Stop Loss (Pips)</label>
<input
type="number"
value={config.stopLossPips}
onChange={(e) => setConfig({ ...config, stopLossPips: Number(e.target.value) })}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 transition-colors"
/>
</div>
<div>
<label className="text-[10px] text-zinc-500 uppercase block mb-1">Take Profit (Pips)</label>
<input
type="number"
value={config.takeProfitPips}
onChange={(e) => setConfig({ ...config, takeProfitPips: Number(e.target.value) })}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 transition-colors"
/>
</div>
</div>
</div>
<div className="mt-auto">
<button
onClick={onRun}
disabled={isRunning}
className={`w-full py-3 rounded-lg flex items-center justify-center gap-2 font-bold text-sm uppercase tracking-wide transition-all ${
isRunning
? 'bg-zinc-800 text-zinc-500 cursor-not-allowed'
: 'bg-blue-700 hover:bg-blue-600 text-white shadow-lg shadow-blue-900/20'
}`}
>
{isRunning ? (
<>Processing Quantum Chain...</>
) : (
<>
<Zap size={16} fill="currentColor" />
Run Backtest
</>
)}
</button>
</div>
</div>
);
};
export default StrategyBuilder;
+38
View File
@@ -0,0 +1,38 @@
import React, { useEffect } from 'react';
import { CheckCircle, AlertCircle, Info } from 'lucide-react';
export type ToastType = 'success' | 'error' | 'info';
interface ToastProps {
message: string;
type: ToastType;
onClose: () => void;
}
const Toast: React.FC<ToastProps> = ({ message, type, onClose }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, 3000);
return () => clearTimeout(timer);
}, [onClose]);
const getIcon = () => {
switch (type) {
case 'success': return <CheckCircle size={18} className="text-emerald-400" />;
case 'error': return <AlertCircle size={18} className="text-red-400" />;
default: return <Info size={18} className="text-blue-400" />;
}
};
return (
<div className="fixed bottom-4 right-4 z-[120] animate-slideUp">
<div className="flex items-center gap-3 px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg shadow-xl">
{getIcon()}
<span className="text-sm text-white">{message}</span>
</div>
</div>
);
};
export default Toast;
+203
View File
@@ -0,0 +1,203 @@
import React, { useState } from 'react';
import {
Search, Layers, PencilRuler, Repeat, Lightbulb, GraduationCap,
Grid3x3, List as ListIcon, Upload, Trash2, X, Archive, Plus
} from 'lucide-react';
import { VaultItem } from '../types';
interface VaultModalProps {
isOpen: boolean;
onClose: () => void;
items: VaultItem[];
onLoad: (item: VaultItem) => void;
onDelete: (id: number) => void;
onNew: () => void;
}
const VaultModal: React.FC<VaultModalProps> = ({ isOpen, onClose, items, onLoad, onDelete, onNew }) => {
const [selectedId, setSelectedId] = useState<number | null>(null);
const [filterCategory, setFilterCategory] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
if (!isOpen) return null;
const filteredItems = items.filter(item => {
const matchesSearch = item.title.toLowerCase().includes(search.toLowerCase());
const matchesCategory = filterCategory ? item.category === filterCategory : true;
return matchesSearch && matchesCategory;
});
const selectedItem = items.find(i => i.id === selectedId);
const getIcon = (cat: string) => {
switch(cat) {
case 'drawing': return <PencilRuler size={14} />;
case 'replay': return <Repeat size={14} />;
case 'idea': return <Lightbulb size={14} />;
case 'lesson': return <GraduationCap size={14} />;
default: return <Layers size={14} />;
}
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
<div className="absolute inset-0 bg-black/70 animate-fadeIn" onClick={onClose} />
<div className="relative w-full max-w-6xl h-[80vh] bg-zinc-900 border border-zinc-800 rounded-xl flex flex-col overflow-hidden animate-slideUp shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-lg">
<Archive size={20} className="text-blue-400" />
</div>
<div>
<h2 className="text-base font-semibold text-white tracking-tight">Trading Vault</h2>
<p className="text-xs text-zinc-500">Your saved analyses, drawings, and trading data</p>
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={onNew} className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-500 rounded transition-colors">
<Plus size={14} />
New Entry
</button>
<button onClick={onClose} className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors">
<X size={18} />
</button>
</div>
</div>
<div className="flex-1 flex overflow-hidden">
{/* Sidebar */}
<div className="w-56 border-r border-zinc-800 p-4 flex flex-col bg-zinc-900/50">
<div className="relative mb-4">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500" />
<input
type="text"
placeholder="Search vault..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full h-9 pl-9 pr-3 text-xs bg-zinc-800/50 border border-zinc-700 rounded-lg text-white placeholder-zinc-500 focus:outline-none focus:border-zinc-600"
/>
</div>
<div className="space-y-1 mb-4">
<span className="text-xs font-medium text-zinc-500 uppercase tracking-wider px-2">Categories</span>
<button onClick={() => setFilterCategory(null)} className={`w-full flex items-center gap-2 px-2 py-2 text-xs rounded-lg transition-colors ${!filterCategory ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-white'}`}>
<Layers size={14} /> All Items
</button>
<button onClick={() => setFilterCategory('drawing')} className={`w-full flex items-center gap-2 px-2 py-2 text-xs rounded-lg transition-colors ${filterCategory === 'drawing' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-white'}`}>
<PencilRuler size={14} /> Drawings
</button>
<button onClick={() => setFilterCategory('replay')} className={`w-full flex items-center gap-2 px-2 py-2 text-xs rounded-lg transition-colors ${filterCategory === 'replay' ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-white'}`}>
<Repeat size={14} /> Replay
</button>
</div>
</div>
{/* Grid/List View */}
<div className="flex-1 flex flex-col overflow-hidden bg-zinc-950/30">
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-800">
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500">Sort by:</span>
<select className="h-7 px-2 text-xs bg-zinc-800 border border-zinc-700 rounded text-white focus:outline-none">
<option>Date Created</option>
<option>Name A-Z</option>
</select>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setViewMode('grid')}
className={`p-1.5 rounded transition-colors ${viewMode === 'grid' ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white'}`}
>
<Grid3x3 size={14} />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-1.5 rounded transition-colors ${viewMode === 'list' ? 'text-white bg-zinc-800' : 'text-zinc-400 hover:text-white'}`}
>
<ListIcon size={14} />
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4">
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredItems.map(item => (
<div
key={item.id}
onClick={() => setSelectedId(item.id)}
className={`p-4 bg-zinc-800/30 border rounded-xl cursor-pointer transition-all ${selectedId === item.id ? 'border-blue-500/50 bg-blue-500/5' : 'border-zinc-800 hover:bg-zinc-800/50'}`}
>
<div className="aspect-video bg-zinc-900 rounded-lg mb-3 flex items-center justify-center">
{getIcon(item.category)}
</div>
<div className="flex items-start justify-between mb-2">
<h3 className="text-sm font-medium text-white line-clamp-1">{item.title}</h3>
<span className={`w-2 h-2 bg-${item.color}-500 rounded-full flex-shrink-0 mt-1.5`}></span>
</div>
<div className="flex items-center gap-2 mb-2">
<span className="px-1.5 py-0.5 text-xs bg-zinc-700 text-zinc-300 rounded">{item.asset}</span>
<span className="px-1.5 py-0.5 text-xs bg-zinc-700 text-zinc-300 rounded">{item.timeframe}</span>
</div>
<p className="text-xs text-zinc-500 line-clamp-2">{item.notes}</p>
</div>
))}
</div>
) : (
<div className="flex flex-col gap-2">
{filteredItems.map(item => (
<div
key={item.id}
onClick={() => setSelectedId(item.id)}
className={`flex items-center gap-4 p-3 bg-zinc-800/30 border rounded-lg cursor-pointer transition-all ${selectedId === item.id ? 'border-blue-500/50 bg-blue-500/5' : 'border-zinc-800 hover:bg-zinc-800/50'}`}
>
<div className="w-10 h-10 bg-zinc-900 rounded-lg flex items-center justify-center flex-shrink-0">
{getIcon(item.category)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium text-white truncate">{item.title}</h3>
<span className={`w-2 h-2 bg-${item.color}-500 rounded-full`}></span>
</div>
<p className="text-xs text-zinc-500 truncate">{item.notes}</p>
</div>
<div className="flex items-center gap-3 text-xs text-zinc-500">
<span>{item.asset}</span>
<span>{new Date(item.createdAt).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Details Panel */}
{selectedItem && (
<div className="w-80 border-l border-zinc-800 bg-zinc-900 p-4 flex flex-col animate-slideLeft">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-white">Details</h3>
<button onClick={() => setSelectedId(null)} className="p-1 text-zinc-400 hover:text-white"><X size={14} /></button>
</div>
<h4 className="text-lg font-medium text-white mb-2">{selectedItem.title}</h4>
<p className="text-xs text-zinc-400 mb-4">{selectedItem.notes}</p>
<div className="mt-auto flex gap-2">
<button onClick={() => { onLoad(selectedItem); onClose(); }} className="flex-1 py-2 text-xs font-medium text-white bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors">
Load
</button>
<button onClick={() => onDelete(selectedItem.id)} className="px-3 py-2 text-xs text-red-400 bg-red-500/10 hover:bg-red-500/20 rounded-lg transition-colors">
<Trash2 size={14} />
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default VaultModal;
+49
View File
@@ -0,0 +1,49 @@
@import "tailwindcss";
@layer base {
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: #09090b;
}
::-webkit-scrollbar-thumb {
background: #27272a;
border-radius: 2px;
}
body {
@apply bg-zinc-950 text-zinc-200 antialiased;
font-family: 'Inter', -apple-system, sans-serif;
}
}
.mono {
font-family: 'JetBrains Mono', monospace;
}
.glass {
background: rgba(24, 24, 27, 0.95);
backdrop-filter: blur(20px);
}
input[type="range"] {
-webkit-appearance: none;
background: transparent;
}
input[type="range"]::-webkit-slider-track {
height: 4px;
background: #27272a;
border-radius: 2px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #3b82f6;
border-radius: 50%;
margin-top: -5px;
cursor: pointer;
}
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+175
View File
@@ -0,0 +1,175 @@
export interface OHLCData {
time: number;
open: number;
high: number;
low: number;
close: number;
}
export interface VolumeData {
time: number;
value: number;
color: string;
}
export interface VaultItem {
id: number;
title: string;
category: 'drawing' | 'replay' | 'idea' | 'lesson';
asset: string;
timeframe: string;
tags: string[];
notes: string;
createdAt: string;
thumbnail: null | string;
drawingsCount: number;
color: string;
}
export type ToolType =
| 'crosshair' | 'pointer' | 'trendline' | 'hline' | 'vline'
| 'ray' | 'extended' | 'fibretracement' | 'fibextension'
| 'pitchfork' | 'rectangle' | 'circle' | 'triangle'
| 'channel' | 'xabcd' | 'headshoulders' | 'elliott'
| 'text' | 'note' | 'callout' | 'arrow' | 'pricelabel'
| 'pricerange' | 'daterange' | 'longposition' | 'shortposition'
| 'magnet' | 'lock' | 'visibility' | 'delete';
export interface TradeEntry {
id: string;
pair: string;
type: 'LONG' | 'SHORT';
time: string;
entry: number;
exit: number;
lots: number;
pnl: number;
r: number;
status: 'WIN' | 'LOSS' | 'BE';
setup: string;
}
export interface JournalDay {
date: string;
trades: TradeEntry[];
note: string;
mood: 'disciplined' | 'neutral' | 'tilted' | 'fearful' | 'greedy';
}
export type ChartType = 'Candle' | 'Bar' | 'Line' | 'Area';
export interface SessionStats {
balance: number;
startBalance: number;
winRate: number;
tradesCount: number;
wins: number;
losses: number;
pnl: number;
}
// --- QUANTUM BACKTESTER TYPES ---
export interface StrategyCondition {
id: string;
indicator: 'RSI' | 'EMA' | 'Price' | 'Volume' | 'MACD' | 'Bollinger';
operator: '>' | '<' | '==' | 'CrossOver' | 'CrossUnder';
value: number | 'EMA' | 'Price' | 'RSI';
param1?: number; // e.g. Period
}
export interface StrategyConfig {
name: string;
entryConditions: StrategyCondition[];
exitConditions: StrategyCondition[];
stopLossPips: number;
takeProfitPips: number;
}
export interface BacktestResult {
// Core
totalTrades: number;
netProfit: number;
profitFactor: number;
maxDrawdown: number;
maxDrawdownPercent: number;
sharpeRatio: number;
equityCurve: { time: number; value: number }[];
trades: TradeEntry[];
// Detailed MT5 Stats
initialDeposit: number;
grossProfit: number;
grossLoss: number;
expectedPayoff: number;
absoluteDrawdown: number;
relativeDrawdown: number;
relativeDrawdownPercent: number;
shortPositions: number;
shortWon: number;
longPositions: number;
longWon: number;
profitTrades: number;
lossTrades: number;
largestProfitTrade: number;
largestLossTrade: number;
averageProfitTrade: number;
averageLossTrade: number;
maxConsecutiveWins: number;
maxConsecutiveWinsValue: number;
maxConsecutiveLosses: number;
maxConsecutiveLossesValue: number;
ticksModelled: number;
modellingQuality: number;
}
export interface OptimizationParam {
id: string;
name: string;
min: number;
max: number;
step: number;
current: number;
enabled: boolean;
}
export interface OptimizationResult {
pass: number;
params: string;
profit: number;
drawdown: number;
score: number;
}
export interface MLConfig {
modelType: 'LSTM' | 'RandomForest' | 'Transformer';
features: string[]; // e.g., ['RSI', 'MACD', 'Close']
epochs: number;
learningRate: number;
status: 'Idle' | 'Training' | 'Ready';
accuracy: number;
}
export interface BacktestSettings {
algoFile: string;
algoLanguage: 'Rust' | 'Python' | 'C++' | 'JSON';
symbol: string; // "EURUSD" or "Multi-Pair"
timeframe: string;
dateRange: 'Last Month' | 'Last Year' | 'Custom';
startDate?: string;
endDate?: string;
forwardMode: 'No' | '1/2' | '1/3' | '1/4' | 'Custom';
modeling: 'Every Tick' | 'OHLC (Fast)' | 'Open Prices Only';
latency: 'Zero' | 'Random (1-10ms)' | 'Network (50-200ms)';
optimization: 'Disabled' | 'Slow Complete' | 'Fast Genetic'; // NEW
deposit: number;
leverage: number;
visualMode: boolean;
visualSpeed: number; // 1 to 100
}
+37
View File
@@ -0,0 +1,37 @@
import { OHLCData, VolumeData } from '../types';
export function generateOHLCData(count = 500): OHLCData[] {
const data: OHLCData[] = [];
let basePrice = 1.08000;
const startDate = new Date('2024-01-15T00:00:00');
for (let i = 0; i < count; i++) {
const date = new Date(startDate.getTime() + i * 15 * 60 * 1000); // 15 min candles
const volatility = 0.0005 + Math.random() * 0.001;
const change = (Math.random() - 0.48) * volatility;
const open = basePrice;
const close = basePrice + change;
const high = Math.max(open, close) + Math.random() * 0.0003;
const low = Math.min(open, close) - Math.random() * 0.0003;
data.push({
time: Math.floor(date.getTime() / 1000), // Unix timestamp
open: parseFloat(open.toFixed(5)),
high: parseFloat(high.toFixed(5)),
low: parseFloat(low.toFixed(5)),
close: parseFloat(close.toFixed(5)),
});
basePrice = close;
}
return data;
}
export function generateVolumeData(ohlcData: OHLCData[]): VolumeData[] {
return ohlcData.map(d => ({
time: d.time,
value: Math.floor(500 + Math.random() * 2000),
color: d.close >= d.open ? 'rgba(34, 197, 94, 0.4)' : 'rgba(239, 68, 68, 0.4)',
}));
}
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./src/*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}
+24
View File
@@ -0,0 +1,24 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 1431,
strictPort: true,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
}
}
};
});