Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| <html lang="fa" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>HTS - آزمایشگاه بصری استراتژی ترید</title> | |
| <!-- Vazirmatn Font --> | |
| <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=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| /* Colors */ | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f8fafb; | |
| --bg-tertiary: #f0f4f7; | |
| --primary: #2563eb; | |
| --success: #16a34a; | |
| --danger: #dc2626; | |
| --accent: #06b6d4; | |
| --warning: #f59e0b; | |
| /* Component Colors */ | |
| --color-data: #3b82f6; | |
| --color-indicator: #a855f7; | |
| --color-pattern: #ec4899; | |
| --color-logic: #10b981; | |
| --color-risk: #ef4444; | |
| --color-output: #f59e0b; | |
| /* Text */ | |
| --text-primary: #0f172a; | |
| --text-secondary: #475569; | |
| --text-muted: #94a3b8; | |
| /* Glass */ | |
| --glass-bg: rgba(255, 255, 255, 0.65); | |
| --glass-border: rgba(15, 23, 42, 0.08); | |
| } | |
| body { | |
| font-family: 'Vazirmatn', sans-serif; | |
| background: var(--bg-primary); | |
| background-image: | |
| radial-gradient(ellipse at 10% 10%, rgba(37, 99, 235, 0.05) 0%, transparent 50%), | |
| radial-gradient(ellipse at 90% 90%, rgba(6, 182, 212, 0.05) 0%, transparent 50%); | |
| color: var(--text-primary); | |
| overflow: hidden; | |
| } | |
| /* ============= LAYOUT ============= */ | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| } | |
| /* Header */ | |
| .header { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--glass-border); | |
| padding: 1rem 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
| z-index: 100; | |
| } | |
| .header-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-size: 1.25rem; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.625rem 1.25rem; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 10px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(10px); | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .btn:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| color: white; | |
| border: none; | |
| } | |
| .btn-success { | |
| background: linear-gradient(135deg, var(--success), #10b981); | |
| color: white; | |
| border: none; | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* Main Content (3 columns) */ | |
| .main-content { | |
| display: grid; | |
| grid-template-columns: 300px 1fr 350px; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| /* ============= COMPONENT LIBRARY ============= */ | |
| .component-library { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-left: 1px solid var(--glass-border); | |
| padding: 1.5rem; | |
| overflow-y: auto; | |
| } | |
| .library-header { | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-muted); | |
| margin-bottom: 1.5rem; | |
| } | |
| .component-category { | |
| margin-bottom: 2rem; | |
| } | |
| .category-title { | |
| font-size: 0.875rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-bottom: 1rem; | |
| padding: 0.5rem; | |
| background: var(--bg-secondary); | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .category-icon { | |
| font-size: 1.25rem; | |
| } | |
| .component-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| padding: 0.875rem; | |
| margin-bottom: 0.5rem; | |
| background: white; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 10px; | |
| cursor: grab; | |
| transition: all 0.2s; | |
| } | |
| .component-item:hover { | |
| transform: translateX(-4px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); | |
| } | |
| .component-item:active { | |
| cursor: grabbing; | |
| } | |
| .component-item-icon { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.25rem; | |
| flex-shrink: 0; | |
| } | |
| .component-item.data .component-item-icon { background: rgba(59, 130, 246, 0.1); } | |
| .component-item.indicator .component-item-icon { background: rgba(168, 85, 247, 0.1); } | |
| .component-item.pattern .component-item-icon { background: rgba(236, 72, 153, 0.1); } | |
| .component-item.logic .component-item-icon { background: rgba(16, 185, 129, 0.1); } | |
| .component-item.risk .component-item-icon { background: rgba(239, 68, 68, 0.1); } | |
| .component-item.output .component-item-icon { background: rgba(245, 158, 11, 0.1); } | |
| .component-item-info { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .component-item-name { | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .component-item-desc { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| /* ============= CANVAS ============= */ | |
| .canvas-container { | |
| position: relative; | |
| background: var(--bg-secondary); | |
| overflow: hidden; | |
| } | |
| .canvas { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| } | |
| .canvas-grid { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: | |
| linear-gradient(var(--glass-border) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--glass-border) 1px, transparent 1px); | |
| background-size: 30px 30px; | |
| opacity: 0.3; | |
| } | |
| #connectionSvg { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| .canvas-nodes { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 2; | |
| } | |
| /* Node */ | |
| .node { | |
| position: absolute; | |
| min-width: 220px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(8px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 16px; | |
| box-shadow: 0 10px 25px rgba(2, 6, 23, 0.1); | |
| cursor: move; | |
| transition: box-shadow 0.2s; | |
| user-select: none; | |
| } | |
| .node:hover { | |
| box-shadow: 0 15px 35px rgba(2, 6, 23, 0.15); | |
| } | |
| .node.selected { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); | |
| } | |
| .node-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 12px; | |
| border-bottom: 1px solid var(--glass-border); | |
| } | |
| .node-icon { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.125rem; | |
| flex-shrink: 0; | |
| } | |
| .node.data .node-icon { background: rgba(59, 130, 246, 0.15); } | |
| .node.indicator .node-icon { background: rgba(168, 85, 247, 0.15); } | |
| .node.pattern .node-icon { background: rgba(236, 72, 153, 0.15); } | |
| .node.logic .node-icon { background: rgba(16, 185, 129, 0.15); } | |
| .node.risk .node-icon { background: rgba(239, 68, 68, 0.15); } | |
| .node.output .node-icon { background: rgba(245, 158, 11, 0.15); } | |
| .node-title { | |
| flex: 1; | |
| font-weight: 800; | |
| font-size: 0.875rem; | |
| } | |
| .node-delete { | |
| width: 24px; | |
| height: 24px; | |
| border: none; | |
| background: rgba(220, 38, 38, 0.1); | |
| color: var(--danger); | |
| border-radius: 6px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| transition: all 0.2s; | |
| font-size: 0.875rem; | |
| } | |
| .node:hover .node-delete { | |
| opacity: 1; | |
| } | |
| .node-delete:hover { | |
| background: var(--danger); | |
| color: white; | |
| } | |
| .node-body { | |
| padding: 12px; | |
| } | |
| .node-param { | |
| margin-bottom: 12px; | |
| } | |
| .node-param:last-child { | |
| margin-bottom: 0; | |
| } | |
| .param-label { | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.375rem; | |
| display: block; | |
| } | |
| .param-input { | |
| width: 100%; | |
| padding: 0.5rem; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 8px; | |
| background: white; | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| } | |
| .param-input:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| } | |
| .param-range { | |
| width: 100%; | |
| margin: 0.5rem 0; | |
| } | |
| .range-value { | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--primary); | |
| } | |
| .node-ports { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0 12px 12px; | |
| } | |
| .port { | |
| width: 14px; | |
| height: 14px; | |
| border: 2px solid; | |
| border-radius: 50%; | |
| background: white; | |
| cursor: crosshair; | |
| transition: all 0.2s; | |
| } | |
| .port:hover { | |
| transform: scale(1.4); | |
| box-shadow: 0 0 15px currentColor; | |
| } | |
| .port.input { | |
| border-color: var(--success); | |
| } | |
| .port.output { | |
| border-color: var(--danger); | |
| } | |
| .port.connected { | |
| background: currentColor; | |
| } | |
| .status-indicator { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| margin-right: auto; | |
| } | |
| .status-indicator.active { | |
| background: var(--success); | |
| animation: pulse-indicator 1.5s infinite; | |
| } | |
| @keyframes pulse-indicator { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| /* Connections */ | |
| .connection-path { | |
| fill: none; | |
| stroke: var(--primary); | |
| stroke-width: 3; | |
| stroke-linecap: round; | |
| opacity: 0.6; | |
| transition: all 0.2s; | |
| } | |
| .connection-path:hover { | |
| stroke-width: 4; | |
| opacity: 1; | |
| } | |
| .connection-pulse { | |
| fill: var(--primary); | |
| filter: drop-shadow(0 4px 8px rgba(37, 99, 235, 0.4)); | |
| } | |
| /* ============= PROPERTIES PANEL ============= */ | |
| .properties-panel { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-right: 1px solid var(--glass-border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .panel-tabs { | |
| display: flex; | |
| border-bottom: 1px solid var(--glass-border); | |
| background: var(--bg-secondary); | |
| } | |
| .panel-tab { | |
| flex: 1; | |
| padding: 0.875rem; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-secondary); | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .panel-tab:hover { | |
| color: var(--text-primary); | |
| } | |
| .panel-tab.active { | |
| color: var(--primary); | |
| border-bottom: 2px solid var(--primary); | |
| } | |
| .panel-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 1.5rem; | |
| } | |
| .panel-section { | |
| margin-bottom: 2rem; | |
| } | |
| .section-title { | |
| font-size: 0.875rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-bottom: 1rem; | |
| padding-bottom: 0.5rem; | |
| border-bottom: 1px solid var(--glass-border); | |
| } | |
| .metric { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0.75rem 0; | |
| font-size: 0.875rem; | |
| border-bottom: 1px solid var(--glass-border); | |
| } | |
| .metric:last-child { | |
| border-bottom: none; | |
| } | |
| .metric-label { | |
| color: var(--text-secondary); | |
| } | |
| .metric-value { | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| } | |
| .metric-value.success { color: var(--success); } | |
| .metric-value.danger { color: var(--danger); } | |
| .no-selection { | |
| text-align: center; | |
| padding: 3rem 1rem; | |
| color: var(--text-muted); | |
| } | |
| .no-selection-icon { | |
| font-size: 4rem; | |
| opacity: 0.3; | |
| margin-bottom: 1rem; | |
| } | |
| /* Chart */ | |
| #equityChart { | |
| width: 100%; | |
| height: 250px; | |
| background: white; | |
| border-radius: 12px; | |
| margin-top: 1rem; | |
| } | |
| /* Progress Bar */ | |
| .progress-container { | |
| width: 100%; | |
| height: 6px; | |
| background: var(--bg-tertiary); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| margin: 1rem 0; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--success), var(--accent)); | |
| width: 0%; | |
| transition: width 0.3s; | |
| } | |
| /* Template Card */ | |
| .template-card { | |
| padding: 1rem; | |
| background: white; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 12px; | |
| margin-bottom: 1rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .template-card:hover { | |
| border-color: var(--primary); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); | |
| } | |
| .template-name { | |
| font-size: 0.875rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-bottom: 0.5rem; | |
| } | |
| .template-desc { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| } | |
| /* Results Section */ | |
| .results-section { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-top: 1px solid var(--glass-border); | |
| padding: 1rem 1.5rem; | |
| } | |
| .results-header { | |
| font-size: 0.875rem; | |
| font-weight: 700; | |
| margin-bottom: 1rem; | |
| } | |
| .results-metrics { | |
| display: flex; | |
| gap: 2rem; | |
| flex-wrap: wrap; | |
| } | |
| .result-metric { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .result-metric-label { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| } | |
| .result-metric-value { | |
| font-size: 1.25rem; | |
| font-weight: 800; | |
| } | |
| /* Utilities */ | |
| .hidden { | |
| display: none ; | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-tertiary); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--glass-border); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-muted); | |
| } | |
| /* Toast */ | |
| .toast-container { | |
| position: fixed; | |
| top: 1rem; | |
| left: 1rem; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .toast { | |
| padding: 1rem 1.5rem; | |
| background: white; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 12px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); | |
| min-width: 300px; | |
| animation: slideIn 0.3s ease-out; | |
| } | |
| .toast.success { border-right: 3px solid var(--success); } | |
| .toast.error { border-right: 3px solid var(--danger); } | |
| .toast.info { border-right: 3px solid var(--primary); } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateY(-20px); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| </style> | |
| <!-- API Configuration - Smart Fallback System --> | |
| <script src="/static/js/api-config.js"></script> | |
| <script> | |
| // Initialize API client | |
| window.apiReady = new Promise((resolve) => { | |
| if (window.apiClient) { | |
| console.log('✅ API Client ready'); | |
| resolve(window.apiClient); | |
| } else { | |
| console.error('❌ API Client not loaded'); | |
| } | |
| }); | |
| </script> | |
| </head> | |
| <body> | |
| <!-- Toast Container --> | |
| <div class="toast-container" id="toastContainer"></div> | |
| <div class="app-container"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-title"> | |
| <span>🎯</span> | |
| <span>HTS - آزمایشگاه بصری استراتژی ترید</span> | |
| </div> | |
| <div class="header-actions"> | |
| <button class="btn btn-success" id="executeBtn"> | |
| <span>▶️</span> | |
| <span>اجرا</span> | |
| </button> | |
| <button class="btn" id="pauseBtn" disabled> | |
| <span>⏸️</span> | |
| <span>توقف</span> | |
| </button> | |
| <button class="btn" id="resetBtn"> | |
| <span>🔄</span> | |
| <span>ریست</span> | |
| </button> | |
| <button class="btn btn-primary" id="saveBtn"> | |
| <span>💾</span> | |
| <span>ذخیره</span> | |
| </button> | |
| <button class="btn" id="loadBtn"> | |
| <span>📁</span> | |
| <span>بارگذاری</span> | |
| </button> | |
| <button class="btn" id="exportBtn"> | |
| <span>📤</span> | |
| <span>خروجی</span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <!-- Component Library --> | |
| <aside class="component-library"> | |
| <div class="library-header">📦 کتابخانه اجزا</div> | |
| <!-- Data Sources --> | |
| <div class="component-category"> | |
| <div class="category-title"> | |
| <span class="category-icon">📊</span> | |
| <span>منابع داده</span> | |
| </div> | |
| <div class="component-item data" draggable="true" data-type="price-data"> | |
| <div class="component-item-icon">📊</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">داده قیمت</div> | |
| <div class="component-item-desc">OHLCV لحظهای</div> | |
| </div> | |
| </div> | |
| <div class="component-item data" draggable="true" data-type="multi-timeframe"> | |
| <div class="component-item-icon">⏱️</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">چند تایمفریم</div> | |
| <div class="component-item-desc">تحلیل MTF</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Indicators --> | |
| <div class="component-category"> | |
| <div class="category-title"> | |
| <span class="category-icon">📈</span> | |
| <span>اندیکاتورها</span> | |
| </div> | |
| <div class="component-item indicator" draggable="true" data-type="rsi"> | |
| <div class="component-item-icon">📉</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">RSI</div> | |
| <div class="component-item-desc">شاخص قدرت نسبی</div> | |
| </div> | |
| </div> | |
| <div class="component-item indicator" draggable="true" data-type="macd"> | |
| <div class="component-item-icon">〰️</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">MACD</div> | |
| <div class="component-item-desc">واگرایی میانگین</div> | |
| </div> | |
| </div> | |
| <div class="component-item indicator" draggable="true" data-type="ema"> | |
| <div class="component-item-icon">📈</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">EMA</div> | |
| <div class="component-item-desc">میانگین نمایی</div> | |
| </div> | |
| </div> | |
| <div class="component-item indicator" draggable="true" data-type="bollinger"> | |
| <div class="component-item-icon">🎯</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">Bollinger Bands</div> | |
| <div class="component-item-desc">باندهای بولینگر</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pattern Recognition --> | |
| <div class="component-category"> | |
| <div class="category-title"> | |
| <span class="category-icon">🎯</span> | |
| <span>تشخیص الگو</span> | |
| </div> | |
| <div class="component-item pattern" draggable="true" data-type="smc"> | |
| <div class="component-item-icon">💎</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">Smart Money</div> | |
| <div class="component-item-desc">مفاهیم SMC</div> | |
| </div> | |
| </div> | |
| <div class="component-item pattern" draggable="true" data-type="fibonacci"> | |
| <div class="component-item-icon">📐</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">فیبوناچی</div> | |
| <div class="component-item-desc">سطوح فیبوناچی</div> | |
| </div> | |
| </div> | |
| <div class="component-item pattern" draggable="true" data-type="harmonic"> | |
| <div class="component-item-icon">🎼</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">هارمونیک</div> | |
| <div class="component-item-desc">الگوهای هارمونیک</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Logic Operators --> | |
| <div class="component-category"> | |
| <div class="category-title"> | |
| <span class="category-icon">🧠</span> | |
| <span>عملگرهای منطقی</span> | |
| </div> | |
| <div class="component-item logic" draggable="true" data-type="and-gate"> | |
| <div class="component-item-icon">∧</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">دروازه AND</div> | |
| <div class="component-item-desc">منطق و</div> | |
| </div> | |
| </div> | |
| <div class="component-item logic" draggable="true" data-type="or-gate"> | |
| <div class="component-item-icon">∨</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">دروازه OR</div> | |
| <div class="component-item-desc">منطق یا</div> | |
| </div> | |
| </div> | |
| <div class="component-item logic" draggable="true" data-type="threshold"> | |
| <div class="component-item-icon">🎚️</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">آستانه</div> | |
| <div class="component-item-desc">مقایسه مقدار</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Risk Management --> | |
| <div class="component-category"> | |
| <div class="category-title"> | |
| <span class="category-icon">💰</span> | |
| <span>مدیریت ریسک</span> | |
| </div> | |
| <div class="component-item risk" draggable="true" data-type="position-sizer"> | |
| <div class="component-item-icon">💰</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">اندازه پوزیشن</div> | |
| <div class="component-item-desc">محاسبه حجم</div> | |
| </div> | |
| </div> | |
| <div class="component-item risk" draggable="true" data-type="stop-loss"> | |
| <div class="component-item-icon">🛑</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">استاپ لاس</div> | |
| <div class="component-item-desc">محاسبه SL</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Output --> | |
| <div class="component-category"> | |
| <div class="category-title"> | |
| <span class="category-icon">📤</span> | |
| <span>خروجی/اقدامات</span> | |
| </div> | |
| <div class="component-item output" draggable="true" data-type="signal-output"> | |
| <div class="component-item-icon">📤</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">خروجی سیگنال</div> | |
| <div class="component-item-desc">نتیجه نهایی</div> | |
| </div> | |
| </div> | |
| <div class="component-item output" draggable="true" data-type="trade-executor"> | |
| <div class="component-item-icon">⚡</div> | |
| <div class="component-item-info"> | |
| <div class="component-item-name">اجرای معامله</div> | |
| <div class="component-item-desc">اجرای خودکار</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Canvas --> | |
| <div class="canvas-container"> | |
| <div class="canvas"> | |
| <div class="canvas-grid"></div> | |
| <svg id="connectionSvg"> | |
| <defs> | |
| <linearGradient id="gradBuy" x1="0%" y1="0%" x2="100%" y2="0%"> | |
| <stop offset="0%" style="stop-color:#16a34a;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#10b981;stop-opacity:1" /> | |
| </linearGradient> | |
| <linearGradient id="gradSell" x1="0%" y1="0%" x2="100%" y2="0%"> | |
| <stop offset="0%" style="stop-color:#dc2626;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#ef4444;stop-opacity:1" /> | |
| </linearGradient> | |
| <linearGradient id="gradHold" x1="0%" y1="0%" x2="100%" y2="0%"> | |
| <stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#06b6d4;stop-opacity:1" /> | |
| </linearGradient> | |
| </defs> | |
| </svg> | |
| <div class="canvas-nodes" id="canvasNodes"></div> | |
| </div> | |
| </div> | |
| <!-- Properties Panel --> | |
| <aside class="properties-panel"> | |
| <div class="panel-tabs"> | |
| <button class="panel-tab active" data-tab="properties">ویژگیها</button> | |
| <button class="panel-tab" data-tab="results">نتایج</button> | |
| <button class="panel-tab" data-tab="templates">قالبها</button> | |
| </div> | |
| <div class="panel-content"> | |
| <!-- Properties Tab --> | |
| <div id="propertiesTab" class="panel-tab-content"> | |
| <div class="no-selection"> | |
| <div class="no-selection-icon">📦</div> | |
| <p style="font-weight: 600; margin-bottom: 0.5rem;">هیچ گرهای انتخاب نشده</p> | |
| <p style="font-size: 0.875rem;">یک گره را از کتابخانه به Canvas بکشید</p> | |
| </div> | |
| </div> | |
| <!-- Results Tab --> | |
| <div id="resultsTab" class="panel-tab-content hidden"> | |
| <div class="panel-section"> | |
| <div class="section-title">📊 عملکرد کلی</div> | |
| <div class="metric"> | |
| <span class="metric-label">تعداد معاملات</span> | |
| <span class="metric-value" id="totalTrades">۰</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">نرخ برد</span> | |
| <span class="metric-value success" id="winRate">۰٪</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">سود/زیان کل</span> | |
| <span class="metric-value" id="totalPnL">$۰</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">حداکثر افت</span> | |
| <span class="metric-value danger" id="maxDrawdown">۰٪</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">فاکتور سود</span> | |
| <span class="metric-value" id="profitFactor">۰</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-label">شارپ</span> | |
| <span class="metric-value" id="sharpeRatio">۰</span> | |
| </div> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="section-title">📈 نمودار سرمایه</div> | |
| <canvas id="equityChart"></canvas> | |
| </div> | |
| <div class="panel-section"> | |
| <div class="section-title">پیشرفت</div> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="progressBar"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Templates Tab --> | |
| <div id="templatesTab" class="panel-tab-content hidden"> | |
| <div class="panel-section"> | |
| <div class="section-title">📋 قالبهای آماده</div> | |
| <div class="template-card" data-template="rsi-macd"> | |
| <div class="template-name">⚡ RSI + MACD Classic</div> | |
| <div class="template-desc">استراتژی کلاسیک ترکیب RSI و MACD برای سیگنالگیری</div> | |
| </div> | |
| <div class="template-card" data-template="smc-mtf"> | |
| <div class="template-name">💎 SMC Multi-Timeframe</div> | |
| <div class="template-desc">استراتژی Smart Money در چند تایمفریم</div> | |
| </div> | |
| <div class="template-card" data-template="bollinger-breakout"> | |
| <div class="template-name">🎯 Bollinger Breakout</div> | |
| <div class="template-desc">شکست باندهای بولینگر با مدیریت ریسک</div> | |
| </div> | |
| <div class="template-card" data-template="trend-following"> | |
| <div class="template-name">📈 Trend Following</div> | |
| <div class="template-desc">دنبال کردن روند با EMA و فیبوناچی</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Results Section --> | |
| <div class="results-section hidden" id="resultsSection"> | |
| <div class="results-header">📊 نتایج زنده - Live Results</div> | |
| <div class="results-metrics"> | |
| <div class="result-metric"> | |
| <div class="result-metric-label">معاملات</div> | |
| <div class="result-metric-value" id="liveTradeCount">۰</div> | |
| </div> | |
| <div class="result-metric"> | |
| <div class="result-metric-label">نرخ برد</div> | |
| <div class="result-metric-value success" id="liveWinRate">۰٪</div> | |
| </div> | |
| <div class="result-metric"> | |
| <div class="result-metric-label">سود</div> | |
| <div class="result-metric-value success" id="liveProfit">$۰</div> | |
| </div> | |
| <div class="result-metric"> | |
| <div class="result-metric-label">افت</div> | |
| <div class="result-metric-value danger" id="liveDrawdown">-۰٪</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Hidden File Input --> | |
| <input type="file" id="fileInput" accept=".json" style="display: none;"> | |
| <script> | |
| // ============= STATE MANAGEMENT ============= | |
| const state = { | |
| nodes: [], | |
| connections: [], | |
| selectedNode: null, | |
| nodeIdCounter: 0, | |
| isExecuting: false, | |
| dragState: { | |
| active: false, | |
| node: null, | |
| offsetX: 0, | |
| offsetY: 0 | |
| }, | |
| connectState: { | |
| active: false, | |
| fromNode: null, | |
| fromPort: null | |
| } | |
| }; | |
| // ============= COMPONENT DEFINITIONS ============= | |
| const NODE_LIBRARY = { | |
| 'price-data': { | |
| name: 'داده قیمت', | |
| category: 'data', | |
| icon: '📊', | |
| params: [ | |
| { name: 'symbol', label: 'نماد', type: 'text', default: 'BTC/USDT' }, | |
| { name: 'timeframe', label: 'تایمفریم', type: 'select', options: ['1m', '5m', '15m', '1h', '4h', '1d'], default: '15m' } | |
| ], | |
| inputs: [], | |
| outputs: ['price', 'volume'] | |
| }, | |
| 'multi-timeframe': { | |
| name: 'چند تایمفریم', | |
| category: 'data', | |
| icon: '⏱️', | |
| params: [ | |
| { name: 'tf1', label: 'تایمفریم ۱', type: 'select', options: ['5m', '15m', '1h', '4h'], default: '15m' }, | |
| { name: 'tf2', label: 'تایمفریم ۲', type: 'select', options: ['5m', '15m', '1h', '4h'], default: '1h' }, | |
| { name: 'tf3', label: 'تایمفریم ۳', type: 'select', options: ['5m', '15m', '1h', '4h'], default: '4h' } | |
| ], | |
| inputs: [], | |
| outputs: ['tf1_data', 'tf2_data', 'tf3_data'] | |
| }, | |
| 'rsi': { | |
| name: 'RSI', | |
| category: 'indicator', | |
| icon: '📉', | |
| params: [ | |
| { name: 'period', label: 'دوره', type: 'range', min: 2, max: 50, default: 14 }, | |
| { name: 'oversold', label: 'اشباع فروش', type: 'range', min: 10, max: 40, default: 30 }, | |
| { name: 'overbought', label: 'اشباع خرید', type: 'range', min: 60, max: 90, default: 70 } | |
| ], | |
| inputs: ['price'], | |
| outputs: ['rsi', 'oversold_signal', 'overbought_signal'] | |
| }, | |
| 'macd': { | |
| name: 'MACD', | |
| category: 'indicator', | |
| icon: '〰️', | |
| params: [ | |
| { name: 'fast', label: 'سریع', type: 'range', min: 5, max: 20, default: 12 }, | |
| { name: 'slow', label: 'کند', type: 'range', min: 20, max: 40, default: 26 }, | |
| { name: 'signal', label: 'سیگنال', type: 'range', min: 5, max: 15, default: 9 } | |
| ], | |
| inputs: ['price'], | |
| outputs: ['macd', 'signal', 'cross_up', 'cross_down'] | |
| }, | |
| 'ema': { | |
| name: 'EMA', | |
| category: 'indicator', | |
| icon: '📈', | |
| params: [ | |
| { name: 'period', label: 'دوره', type: 'range', min: 5, max: 200, default: 20 } | |
| ], | |
| inputs: ['price'], | |
| outputs: ['ema', 'above', 'below'] | |
| }, | |
| 'bollinger': { | |
| name: 'Bollinger Bands', | |
| category: 'indicator', | |
| icon: '🎯', | |
| params: [ | |
| { name: 'period', label: 'دوره', type: 'range', min: 10, max: 50, default: 20 }, | |
| { name: 'std', label: 'انحراف معیار', type: 'range', min: 1, max: 3, step: 0.5, default: 2 } | |
| ], | |
| inputs: ['price'], | |
| outputs: ['upper', 'middle', 'lower', 'above_upper', 'below_lower'] | |
| }, | |
| 'smc': { | |
| name: 'Smart Money', | |
| category: 'pattern', | |
| icon: '💎', | |
| params: [ | |
| { name: 'sensitivity', label: 'حساسیت', type: 'range', min: 0.1, max: 1, step: 0.1, default: 0.7 }, | |
| { name: 'lookback', label: 'بازنگری', type: 'range', min: 20, max: 100, default: 50 } | |
| ], | |
| inputs: ['price'], | |
| outputs: ['order_block', 'fvg', 'score'] | |
| }, | |
| 'fibonacci': { | |
| name: 'فیبوناچی', | |
| category: 'pattern', | |
| icon: '📐', | |
| params: [ | |
| { name: 'direction', label: 'جهت', type: 'select', options: ['Retracement', 'Extension'], default: 'Retracement' } | |
| ], | |
| inputs: ['high', 'low'], | |
| outputs: ['fib_236', 'fib_382', 'fib_500', 'fib_618'] | |
| }, | |
| 'harmonic': { | |
| name: 'هارمونیک', | |
| category: 'pattern', | |
| icon: '🎼', | |
| params: [ | |
| { name: 'pattern', label: 'الگو', type: 'select', options: ['Gartley', 'Butterfly', 'Bat', 'Crab'], default: 'Gartley' } | |
| ], | |
| inputs: ['price'], | |
| outputs: ['pattern_found', 'completion'] | |
| }, | |
| 'and-gate': { | |
| name: 'دروازه AND', | |
| category: 'logic', | |
| icon: '∧', | |
| params: [], | |
| inputs: ['input1', 'input2'], | |
| outputs: ['result'] | |
| }, | |
| 'or-gate': { | |
| name: 'دروازه OR', | |
| category: 'logic', | |
| icon: '∨', | |
| params: [], | |
| inputs: ['input1', 'input2'], | |
| outputs: ['result'] | |
| }, | |
| 'threshold': { | |
| name: 'آستانه', | |
| category: 'logic', | |
| icon: '🎚️', | |
| params: [ | |
| { name: 'threshold', label: 'مقدار آستانه', type: 'range', min: 0, max: 100, default: 50 } | |
| ], | |
| inputs: ['value'], | |
| outputs: ['above', 'below'] | |
| }, | |
| 'position-sizer': { | |
| name: 'اندازه پوزیشن', | |
| category: 'risk', | |
| icon: '💰', | |
| params: [ | |
| { name: 'risk_percent', label: 'درصد ریسک', type: 'range', min: 0.5, max: 5, step: 0.5, default: 2 } | |
| ], | |
| inputs: ['capital', 'atr'], | |
| outputs: ['size', 'risk_amount'] | |
| }, | |
| 'stop-loss': { | |
| name: 'استاپ لاس', | |
| category: 'risk', | |
| icon: '🛑', | |
| params: [ | |
| { name: 'atr_multiplier', label: 'ضریب ATR', type: 'range', min: 1, max: 5, step: 0.5, default: 2 } | |
| ], | |
| inputs: ['entry', 'atr'], | |
| outputs: ['sl_price', 'sl_distance'] | |
| }, | |
| 'signal-output': { | |
| name: 'خروجی سیگنال', | |
| category: 'output', | |
| icon: '📤', | |
| params: [], | |
| inputs: ['buy_signal', 'sell_signal'], | |
| outputs: [] | |
| }, | |
| 'trade-executor': { | |
| name: 'اجرای معامله', | |
| category: 'output', | |
| icon: '⚡', | |
| params: [ | |
| { name: 'commission', label: 'کمیسیون', type: 'range', min: 0, max: 1, step: 0.05, default: 0.1 } | |
| ], | |
| inputs: ['signal', 'size'], | |
| outputs: [] | |
| } | |
| }; | |
| // ============= DOM ELEMENTS ============= | |
| const elements = { | |
| canvasNodes: document.getElementById('canvasNodes'), | |
| connectionSvg: document.getElementById('connectionSvg'), | |
| executeBtn: document.getElementById('executeBtn'), | |
| pauseBtn: document.getElementById('pauseBtn'), | |
| resetBtn: document.getElementById('resetBtn'), | |
| saveBtn: document.getElementById('saveBtn'), | |
| loadBtn: document.getElementById('loadBtn'), | |
| exportBtn: document.getElementById('exportBtn'), | |
| fileInput: document.getElementById('fileInput'), | |
| toastContainer: document.getElementById('toastContainer'), | |
| propertiesTab: document.getElementById('propertiesTab'), | |
| resultsTab: document.getElementById('resultsTab'), | |
| templatesTab: document.getElementById('templatesTab'), | |
| resultsSection: document.getElementById('resultsSection'), | |
| progressBar: document.getElementById('progressBar') | |
| }; | |
| // ============= UTILITY FUNCTIONS ============= | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.textContent = message; | |
| elements.toastContainer.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.animation = 'slideIn 0.3s ease-out reverse'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| function generateId() { | |
| return `node-${state.nodeIdCounter++}`; | |
| } | |
| function toFarsi(num) { | |
| const farsiDigits = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; | |
| return String(num).replace(/\d/g, d => farsiDigits[d]); | |
| } | |
| // ============= NODE CREATION ============= | |
| function createNode(type, x, y, params = null) { | |
| const definition = NODE_LIBRARY[type]; | |
| if (!definition) return null; | |
| const nodeId = generateId(); | |
| const node = { | |
| id: nodeId, | |
| type: type, | |
| definition: definition, | |
| x: x, | |
| y: y, | |
| params: {} | |
| }; | |
| // Initialize parameters | |
| definition.params.forEach(param => { | |
| node.params[param.name] = params && params[param.name] !== undefined ? | |
| params[param.name] : param.default; | |
| }); | |
| state.nodes.push(node); | |
| renderNode(node); | |
| return node; | |
| } | |
| function renderNode(node) { | |
| const nodeEl = document.createElement('div'); | |
| nodeEl.className = `node ${node.definition.category}`; | |
| nodeEl.id = node.id; | |
| nodeEl.style.left = `${node.x}px`; | |
| nodeEl.style.top = `${node.y}px`; | |
| // Header | |
| const header = document.createElement('div'); | |
| header.className = 'node-header'; | |
| header.innerHTML = ` | |
| <div class="node-icon">${node.definition.icon}</div> | |
| <div class="node-title">${node.definition.name}</div> | |
| <div class="status-indicator"></div> | |
| <button class="node-delete" data-node="${node.id}">✕</button> | |
| `; | |
| // Body | |
| const body = document.createElement('div'); | |
| body.className = 'node-body'; | |
| node.definition.params.forEach(param => { | |
| const paramDiv = document.createElement('div'); | |
| paramDiv.className = 'node-param'; | |
| if (param.type === 'select') { | |
| paramDiv.innerHTML = ` | |
| <label class="param-label">${param.label}</label> | |
| <select class="param-input" data-node="${node.id}" data-param="${param.name}"> | |
| ${param.options.map(opt => `<option value="${opt}" ${opt === node.params[param.name] ? 'selected' : ''}>${opt}</option>`).join('')} | |
| </select> | |
| `; | |
| } else if (param.type === 'range') { | |
| const step = param.step || 1; | |
| paramDiv.innerHTML = ` | |
| <label class="param-label">${param.label}: <span class="range-value">${toFarsi(node.params[param.name])}</span></label> | |
| <input type="range" class="param-range" data-node="${node.id}" data-param="${param.name}" | |
| min="${param.min}" max="${param.max}" step="${step}" value="${node.params[param.name]}"> | |
| `; | |
| } else { | |
| paramDiv.innerHTML = ` | |
| <label class="param-label">${param.label}</label> | |
| <input type="${param.type}" class="param-input" data-node="${node.id}" data-param="${param.name}" | |
| value="${node.params[param.name]}"> | |
| `; | |
| } | |
| body.appendChild(paramDiv); | |
| }); | |
| // Ports | |
| const ports = document.createElement('div'); | |
| ports.className = 'node-ports'; | |
| if (node.definition.inputs.length > 0) { | |
| const inputPort = document.createElement('div'); | |
| inputPort.className = 'port input'; | |
| inputPort.dataset.node = node.id; | |
| inputPort.dataset.type = 'input'; | |
| inputPort.title = 'ورودی'; | |
| ports.appendChild(inputPort); | |
| } | |
| if (node.definition.outputs.length > 0) { | |
| const outputPort = document.createElement('div'); | |
| outputPort.className = 'port output'; | |
| outputPort.dataset.node = node.id; | |
| outputPort.dataset.type = 'output'; | |
| outputPort.title = 'خروجی'; | |
| ports.appendChild(outputPort); | |
| } | |
| nodeEl.appendChild(header); | |
| if (body.children.length > 0) { | |
| nodeEl.appendChild(body); | |
| } | |
| nodeEl.appendChild(ports); | |
| elements.canvasNodes.appendChild(nodeEl); | |
| setupNodeEvents(nodeEl, node); | |
| } | |
| function setupNodeEvents(nodeEl, node) { | |
| // Node selection | |
| nodeEl.addEventListener('click', (e) => { | |
| if (!e.target.closest('.node-delete') && !e.target.closest('.port')) { | |
| selectNode(node); | |
| } | |
| }); | |
| // Node dragging | |
| nodeEl.addEventListener('mousedown', (e) => { | |
| if (e.target.closest('.port') || e.target.closest('.node-delete') || e.target.closest('.param-input') || e.target.closest('.param-range')) return; | |
| state.dragState.active = true; | |
| state.dragState.node = node; | |
| state.dragState.offsetX = e.clientX - node.x; | |
| state.dragState.offsetY = e.clientY - node.y; | |
| }); | |
| // Parameter changes | |
| nodeEl.querySelectorAll('.param-input, .param-range').forEach(input => { | |
| input.addEventListener('input', (e) => { | |
| const paramName = e.target.dataset.param; | |
| const value = e.target.type === 'range' ? parseFloat(e.target.value) : e.target.value; | |
| node.params[paramName] = value; | |
| // Update range value display | |
| if (e.target.type === 'range') { | |
| const valueSpan = e.target.previousElementSibling.querySelector('.range-value'); | |
| if (valueSpan) { | |
| valueSpan.textContent = toFarsi(value); | |
| } | |
| } | |
| updatePropertiesPanel(); | |
| }); | |
| }); | |
| // Delete button | |
| const deleteBtn = nodeEl.querySelector('.node-delete'); | |
| if (deleteBtn) { | |
| deleteBtn.addEventListener('click', () => deleteNode(node.id)); | |
| } | |
| // Port connections | |
| const ports = nodeEl.querySelectorAll('.port'); | |
| ports.forEach(port => { | |
| port.addEventListener('mousedown', (e) => { | |
| e.stopPropagation(); | |
| startConnection(port, node); | |
| }); | |
| }); | |
| } | |
| function selectNode(node) { | |
| document.querySelectorAll('.node').forEach(el => el.classList.remove('selected')); | |
| state.selectedNode = node; | |
| document.getElementById(node.id).classList.add('selected'); | |
| updatePropertiesPanel(); | |
| } | |
| function deleteNode(nodeId) { | |
| state.connections = state.connections.filter(conn => | |
| conn.from !== nodeId && conn.to !== nodeId | |
| ); | |
| state.nodes = state.nodes.filter(n => n.id !== nodeId); | |
| document.getElementById(nodeId)?.remove(); | |
| updateConnections(); | |
| if (state.selectedNode?.id === nodeId) { | |
| state.selectedNode = null; | |
| updatePropertiesPanel(); | |
| } | |
| } | |
| // ============= CONNECTIONS ============= | |
| function startConnection(portEl, node) { | |
| if (portEl.dataset.type === 'output') { | |
| state.connectState.active = true; | |
| state.connectState.fromNode = node; | |
| state.connectState.fromPort = portEl; | |
| } | |
| } | |
| document.addEventListener('mouseup', (e) => { | |
| if (state.connectState.active) { | |
| const targetPort = e.target.closest('.port'); | |
| if (targetPort && targetPort.dataset.type === 'input') { | |
| const targetNode = state.nodes.find(n => n.id === targetPort.dataset.node); | |
| if (targetNode && targetNode.id !== state.connectState.fromNode.id) { | |
| createConnection(state.connectState.fromNode.id, targetNode.id); | |
| } | |
| } | |
| state.connectState.active = false; | |
| state.connectState.fromNode = null; | |
| state.connectState.fromPort = null; | |
| } | |
| }); | |
| function createConnection(fromNodeId, toNodeId) { | |
| const exists = state.connections.some(c => c.from === fromNodeId && c.to === toNodeId); | |
| if (exists) { | |
| showToast('این اتصال قبلاً وجود دارد', 'info'); | |
| return; | |
| } | |
| state.connections.push({ | |
| id: `conn-${state.connections.length}`, | |
| from: fromNodeId, | |
| to: toNodeId | |
| }); | |
| updateConnections(); | |
| showToast('اتصال ایجاد شد', 'success'); | |
| } | |
| function updateConnections() { | |
| const svg = elements.connectionSvg; | |
| // Clear existing paths (keep defs) | |
| Array.from(svg.children).forEach(child => { | |
| if (child.tagName !== 'defs') { | |
| child.remove(); | |
| } | |
| }); | |
| state.connections.forEach(conn => { | |
| const fromNode = state.nodes.find(n => n.id === conn.from); | |
| const toNode = state.nodes.find(n => n.id === conn.to); | |
| if (!fromNode || !toNode) return; | |
| const fromEl = document.getElementById(fromNode.id); | |
| const toEl = document.getElementById(toNode.id); | |
| if (!fromEl || !toEl) return; | |
| const fromPort = fromEl.querySelector('.port.output'); | |
| const toPort = toEl.querySelector('.port.input'); | |
| if (!fromPort || !toPort) return; | |
| const fromRect = fromPort.getBoundingClientRect(); | |
| const toRect = toPort.getBoundingClientRect(); | |
| const containerRect = svg.getBoundingClientRect(); | |
| const x1 = fromRect.left + fromRect.width / 2 - containerRect.left; | |
| const y1 = fromRect.top + fromRect.height / 2 - containerRect.top; | |
| const x2 = toRect.left + toRect.width / 2 - containerRect.left; | |
| const y2 = toRect.top + toRect.height / 2 - containerRect.top; | |
| const dx = x2 - x1; | |
| const curve = Math.abs(dx) * 0.5; | |
| const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| path.setAttribute('class', 'connection-path'); | |
| path.setAttribute('d', `M ${x1} ${y1} C ${x1 + curve} ${y1}, ${x2 - curve} ${y2}, ${x2} ${y2}`); | |
| path.dataset.connection = conn.id; | |
| svg.appendChild(path); | |
| fromPort.classList.add('connected'); | |
| toPort.classList.add('connected'); | |
| }); | |
| } | |
| // ============= DRAG AND DROP ============= | |
| document.querySelectorAll('.component-item').forEach(item => { | |
| item.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('componentType', item.dataset.type); | |
| }); | |
| }); | |
| elements.canvasNodes.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| }); | |
| elements.canvasNodes.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| const componentType = e.dataTransfer.getData('componentType'); | |
| if (componentType) { | |
| const rect = elements.canvasNodes.getBoundingClientRect(); | |
| const x = e.clientX - rect.left - 110; | |
| const y = e.clientY - rect.top - 50; | |
| createNode(componentType, x, y); | |
| showToast('گره اضافه شد', 'success'); | |
| } | |
| }); | |
| // Node movement | |
| document.addEventListener('mousemove', (e) => { | |
| if (state.dragState.active) { | |
| const node = state.dragState.node; | |
| node.x = e.clientX - state.dragState.offsetX; | |
| node.y = e.clientY - state.dragState.offsetY; | |
| const nodeEl = document.getElementById(node.id); | |
| nodeEl.style.left = `${node.x}px`; | |
| nodeEl.style.top = `${node.y}px`; | |
| updateConnections(); | |
| } | |
| }); | |
| document.addEventListener('mouseup', () => { | |
| if (state.dragState.active) { | |
| state.dragState.active = false; | |
| state.dragState.node = null; | |
| } | |
| }); | |
| // ============= PROPERTIES PANEL ============= | |
| function updatePropertiesPanel() { | |
| const tab = elements.propertiesTab; | |
| if (!state.selectedNode) { | |
| tab.innerHTML = ` | |
| <div class="no-selection"> | |
| <div class="no-selection-icon">📦</div> | |
| <p style="font-weight: 600; margin-bottom: 0.5rem;">هیچ گرهای انتخاب نشده</p> | |
| <p style="font-size: 0.875rem;">یک گره را از کتابخانه به Canvas بکشید</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| const node = state.selectedNode; | |
| let html = `<div class="panel-section">`; | |
| html += `<div class="section-title">${node.definition.icon} ${node.definition.name}</div>`; | |
| if (node.definition.params.length > 0) { | |
| node.definition.params.forEach(param => { | |
| const value = node.params[param.name]; | |
| html += `<div class="metric">`; | |
| html += `<span class="metric-label">${param.label}</span>`; | |
| html += `<span class="metric-value">${toFarsi(value)}</span>`; | |
| html += `</div>`; | |
| }); | |
| } else { | |
| html += `<p style="color: var(--text-muted); font-size: 0.875rem;">این گره پارامتری ندارد</p>`; | |
| } | |
| html += `</div>`; | |
| const inputs = state.connections.filter(c => c.to === node.id); | |
| const outputs = state.connections.filter(c => c.from === node.id); | |
| if (inputs.length > 0 || outputs.length > 0) { | |
| html += `<div class="panel-section">`; | |
| html += `<div class="section-title">🔗 اتصالات</div>`; | |
| if (inputs.length > 0) { | |
| html += `<div class="metric"><span class="metric-label">ورودی</span><span class="metric-value">${toFarsi(inputs.length)}</span></div>`; | |
| } | |
| if (outputs.length > 0) { | |
| html += `<div class="metric"><span class="metric-label">خروجی</span><span class="metric-value">${toFarsi(outputs.length)}</span></div>`; | |
| } | |
| html += `</div>`; | |
| } | |
| tab.innerHTML = html; | |
| } | |
| // ============= PANEL TABS ============= | |
| document.querySelectorAll('.panel-tab').forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| document.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active')); | |
| tab.classList.add('active'); | |
| const tabName = tab.dataset.tab; | |
| elements.propertiesTab.classList.add('hidden'); | |
| elements.resultsTab.classList.add('hidden'); | |
| elements.templatesTab.classList.add('hidden'); | |
| if (tabName === 'properties') { | |
| elements.propertiesTab.classList.remove('hidden'); | |
| } else if (tabName === 'results') { | |
| elements.resultsTab.classList.remove('hidden'); | |
| } else if (tabName === 'templates') { | |
| elements.templatesTab.classList.remove('hidden'); | |
| } | |
| }); | |
| }); | |
| // ============= EXECUTION ENGINE ============= | |
| async function executeStrategy() { | |
| if (state.nodes.length === 0) { | |
| showToast('هیچ گرهای برای اجرا وجود ندارد', 'error'); | |
| return; | |
| } | |
| state.isExecuting = true; | |
| elements.executeBtn.disabled = true; | |
| elements.pauseBtn.disabled = false; | |
| elements.resultsSection.classList.remove('hidden'); | |
| showToast('اجرای استراتژی شروع شد...', 'info'); | |
| // Simulate backtest | |
| const numCandles = 100; | |
| for (let i = 0; i < numCandles; i++) { | |
| if (!state.isExecuting) break; | |
| elements.progressBar.style.width = `${(i / numCandles) * 100}%`; | |
| // Simulate processing delay | |
| await new Promise(resolve => setTimeout(resolve, 50)); | |
| } | |
| // Generate results | |
| const results = { | |
| totalTrades: Math.floor(Math.random() * 100 + 50), | |
| winRate: (Math.random() * 40 + 40).toFixed(1), | |
| totalPnL: ((Math.random() - 0.3) * 10000).toFixed(2), | |
| maxDrawdown: (Math.random() * 30).toFixed(1), | |
| profitFactor: (Math.random() * 2 + 0.5).toFixed(2), | |
| sharpeRatio: (Math.random() * 2).toFixed(2) | |
| }; | |
| document.getElementById('totalTrades').textContent = toFarsi(results.totalTrades); | |
| document.getElementById('winRate').textContent = toFarsi(results.winRate) + '٪'; | |
| document.getElementById('totalPnL').textContent = '$' + toFarsi(results.totalPnL); | |
| document.getElementById('totalPnL').className = `metric-value ${parseFloat(results.totalPnL) > 0 ? 'success' : 'danger'}`; | |
| document.getElementById('maxDrawdown').textContent = toFarsi(results.maxDrawdown) + '٪'; | |
| document.getElementById('profitFactor').textContent = toFarsi(results.profitFactor); | |
| document.getElementById('sharpeRatio').textContent = toFarsi(results.sharpeRatio); | |
| document.getElementById('liveTradeCount').textContent = toFarsi(results.totalTrades); | |
| document.getElementById('liveWinRate').textContent = toFarsi(results.winRate) + '٪'; | |
| document.getElementById('liveProfit').textContent = '$' + toFarsi(results.totalPnL); | |
| document.getElementById('liveDrawdown').textContent = '-' + toFarsi(results.maxDrawdown) + '٪'; | |
| // Draw equity curve | |
| drawEquityCurve(); | |
| showToast('اجرای استراتژی به پایان رسید', 'success'); | |
| state.isExecuting = false; | |
| elements.executeBtn.disabled = false; | |
| elements.pauseBtn.disabled = true; | |
| } | |
| function drawEquityCurve() { | |
| const canvas = document.getElementById('equityChart'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = canvas.offsetWidth; | |
| canvas.height = canvas.offsetHeight; | |
| const numPoints = 100; | |
| const points = []; | |
| let y = canvas.height / 2; | |
| for (let i = 0; i <= numPoints; i++) { | |
| y += (Math.random() - 0.45) * 15; | |
| y = Math.max(20, Math.min(canvas.height - 20, y)); | |
| points.push(y); | |
| } | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Grid | |
| ctx.strokeStyle = '#e2e8f0'; | |
| ctx.lineWidth = 1; | |
| for (let i = 0; i <= 4; i++) { | |
| const y = (i / 4) * canvas.height; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(canvas.width, y); | |
| ctx.stroke(); | |
| } | |
| // Equity curve | |
| ctx.beginPath(); | |
| ctx.strokeStyle = points[points.length - 1] < canvas.height / 2 ? '#ef4444' : '#10b981'; | |
| ctx.lineWidth = 3; | |
| points.forEach((y, i) => { | |
| const x = (i / numPoints) * canvas.width; | |
| if (i === 0) { | |
| ctx.moveTo(x, y); | |
| } else { | |
| ctx.lineTo(x, y); | |
| } | |
| }); | |
| ctx.stroke(); | |
| } | |
| function pauseStrategy() { | |
| state.isExecuting = false; | |
| showToast('استراتژی متوقف شد', 'info'); | |
| } | |
| function resetStrategy() { | |
| state.isExecuting = false; | |
| elements.executeBtn.disabled = false; | |
| elements.pauseBtn.disabled = true; | |
| elements.resultsSection.classList.add('hidden'); | |
| elements.progressBar.style.width = '0%'; | |
| document.getElementById('totalTrades').textContent = '۰'; | |
| document.getElementById('winRate').textContent = '۰٪'; | |
| document.getElementById('totalPnL').textContent = '$۰'; | |
| document.getElementById('maxDrawdown').textContent = '۰٪'; | |
| document.getElementById('profitFactor').textContent = '۰'; | |
| document.getElementById('sharpeRatio').textContent = '۰'; | |
| showToast('استراتژی بازنشانی شد', 'info'); | |
| } | |
| // ============= SAVE/LOAD ============= | |
| function saveStrategy() { | |
| const strategy = { | |
| name: 'My Strategy', | |
| version: '1.0', | |
| created: new Date().toISOString(), | |
| nodes: state.nodes.map(n => ({ | |
| id: n.id, | |
| type: n.type, | |
| x: n.x, | |
| y: n.y, | |
| params: n.params | |
| })), | |
| connections: state.connections | |
| }; | |
| const blob = new Blob([JSON.stringify(strategy, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `strategy-${Date.now()}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| showToast('استراتژی ذخیره شد', 'success'); | |
| } | |
| function loadStrategy() { | |
| elements.fileInput.click(); | |
| } | |
| elements.fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| try { | |
| const strategy = JSON.parse(event.target.result); | |
| // Clear current | |
| state.nodes.forEach(n => deleteNode(n.id)); | |
| state.nodes = []; | |
| state.connections = []; | |
| // Load nodes | |
| strategy.nodes.forEach(nodeData => { | |
| createNode(nodeData.type, nodeData.x, nodeData.y, nodeData.params); | |
| }); | |
| // Load connections | |
| state.connections = strategy.connections; | |
| updateConnections(); | |
| showToast('استراتژی بارگذاری شد', 'success'); | |
| } catch (error) { | |
| showToast('خطا در بارگذاری فایل', 'error'); | |
| console.error(error); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| e.target.value = ''; | |
| }); | |
| function exportStrategy() { | |
| showToast('خروجی در حال تولید...', 'info'); | |
| setTimeout(() => { | |
| saveStrategy(); | |
| }, 500); | |
| } | |
| // ============= TEMPLATES ============= | |
| document.querySelectorAll('.template-card').forEach(card => { | |
| card.addEventListener('click', () => { | |
| const templateName = card.dataset.template; | |
| loadTemplate(templateName); | |
| }); | |
| }); | |
| function loadTemplate(templateName) { | |
| // Clear current | |
| state.nodes.forEach(n => deleteNode(n.id)); | |
| state.nodes = []; | |
| state.connections = []; | |
| const templates = { | |
| 'rsi-macd': () => { | |
| const price = createNode('price-data', 100, 100); | |
| const rsi = createNode('rsi', 350, 80); | |
| const macd = createNode('macd', 350, 250); | |
| const andGate = createNode('and-gate', 600, 165); | |
| const output = createNode('signal-output', 850, 165); | |
| setTimeout(() => { | |
| createConnection(price.id, rsi.id); | |
| createConnection(price.id, macd.id); | |
| createConnection(rsi.id, andGate.id); | |
| createConnection(macd.id, andGate.id); | |
| createConnection(andGate.id, output.id); | |
| }, 100); | |
| }, | |
| 'smc-mtf': () => { | |
| const mtf = createNode('multi-timeframe', 100, 150); | |
| const smc = createNode('smc', 400, 150); | |
| const output = createNode('signal-output', 700, 150); | |
| setTimeout(() => { | |
| createConnection(mtf.id, smc.id); | |
| createConnection(smc.id, output.id); | |
| }, 100); | |
| }, | |
| 'bollinger-breakout': () => { | |
| const price = createNode('price-data', 100, 150); | |
| const bollinger = createNode('bollinger', 400, 150); | |
| const positionSizer = createNode('position-sizer', 700, 100); | |
| const executor = createNode('trade-executor', 1000, 150); | |
| setTimeout(() => { | |
| createConnection(price.id, bollinger.id); | |
| createConnection(bollinger.id, executor.id); | |
| createConnection(positionSizer.id, executor.id); | |
| }, 100); | |
| }, | |
| 'trend-following': () => { | |
| const price = createNode('price-data', 100, 150); | |
| const ema = createNode('ema', 400, 150); | |
| const fibonacci = createNode('fibonacci', 700, 150); | |
| const output = createNode('signal-output', 1000, 150); | |
| setTimeout(() => { | |
| createConnection(price.id, ema.id); | |
| createConnection(price.id, fibonacci.id); | |
| createConnection(ema.id, output.id); | |
| createConnection(fibonacci.id, output.id); | |
| }, 100); | |
| } | |
| }; | |
| if (templates[templateName]) { | |
| templates[templateName](); | |
| showToast('قالب بارگذاری شد', 'success'); | |
| } | |
| } | |
| // ============= EVENT LISTENERS ============= | |
| elements.executeBtn.addEventListener('click', executeStrategy); | |
| elements.pauseBtn.addEventListener('click', pauseStrategy); | |
| elements.resetBtn.addEventListener('click', resetStrategy); | |
| elements.saveBtn.addEventListener('click', saveStrategy); | |
| elements.loadBtn.addEventListener('click', loadStrategy); | |
| elements.exportBtn.addEventListener('click', exportStrategy); | |
| // ============= INITIALIZATION ============= | |
| showToast('آزمایشگاه بصری استراتژی آماده است', 'success'); | |
| updatePropertiesPanel(); | |
| // Create sample connections line effect | |
| setInterval(() => { | |
| document.querySelectorAll('.connection-path').forEach(path => { | |
| path.style.opacity = 0.6 + Math.random() * 0.4; | |
| }); | |
| }, 2000); | |
| </script> | |
| </body> | |
| </html> | |