Initial commit: quantum_backtester
@@ -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
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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>
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -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");
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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="<"><</option>
|
||||
<option value=">">></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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)',
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||