| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>HTS - آزمایشگاه بصری استراتژی ترید</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=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"> |
|
|
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| :root { |
| |
| --bg-primary: #ffffff; |
| --bg-secondary: #f8fafb; |
| --bg-tertiary: #f0f4f7; |
| --primary: #2563eb; |
| --success: #16a34a; |
| --danger: #dc2626; |
| --accent: #06b6d4; |
| --warning: #f59e0b; |
| |
| |
| --color-data: #3b82f6; |
| --color-indicator: #a855f7; |
| --color-pattern: #ec4899; |
| --color-logic: #10b981; |
| --color-risk: #ef4444; |
| --color-output: #f59e0b; |
| |
| |
| --text-primary: #0f172a; |
| --text-secondary: #475569; |
| --text-muted: #94a3b8; |
| |
| |
| --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; |
| } |
| |
| |
| .app-container { |
| display: flex; |
| flex-direction: column; |
| height: 100vh; |
| } |
| |
| |
| .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 { |
| display: grid; |
| grid-template-columns: 300px 1fr 350px; |
| flex: 1; |
| overflow: hidden; |
| } |
| |
| |
| .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-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 { |
| 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; } |
| } |
| |
| |
| .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 { |
| 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; |
| } |
| |
| |
| #equityChart { |
| width: 100%; |
| height: 250px; |
| background: white; |
| border-radius: 12px; |
| margin-top: 1rem; |
| } |
| |
| |
| .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 { |
| 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 { |
| 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; |
| } |
| |
| |
| .hidden { |
| display: none !important; |
| } |
| |
| |
| ::-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-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> |
| |
| <script src="/static/js/api-config.js"></script> |
| <script> |
| |
| 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> |
| |
| <div class="toast-container" id="toastContainer"></div> |
|
|
| <div class="app-container"> |
| |
| <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> |
|
|
| |
| <div class="main-content"> |
| |
| <aside class="component-library"> |
| <div class="library-header">📦 کتابخانه اجزا</div> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <input type="file" id="fileInput" accept=".json" style="display: none;"> |
|
|
| <script> |
| |
| 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 |
| } |
| }; |
| |
| |
| 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: [] |
| } |
| }; |
| |
| |
| 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') |
| }; |
| |
| |
| 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]); |
| } |
| |
| |
| 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: {} |
| }; |
| |
| |
| 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`; |
| |
| |
| 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> |
| `; |
| |
| |
| 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); |
| }); |
| |
| |
| 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) { |
| |
| nodeEl.addEventListener('click', (e) => { |
| if (!e.target.closest('.node-delete') && !e.target.closest('.port')) { |
| selectNode(node); |
| } |
| }); |
| |
| |
| 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; |
| }); |
| |
| |
| 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; |
| |
| |
| if (e.target.type === 'range') { |
| const valueSpan = e.target.previousElementSibling.querySelector('.range-value'); |
| if (valueSpan) { |
| valueSpan.textContent = toFarsi(value); |
| } |
| } |
| |
| updatePropertiesPanel(); |
| }); |
| }); |
| |
| |
| const deleteBtn = nodeEl.querySelector('.node-delete'); |
| if (deleteBtn) { |
| deleteBtn.addEventListener('click', () => deleteNode(node.id)); |
| } |
| |
| |
| 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(); |
| } |
| } |
| |
| |
| 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; |
| |
| 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'); |
| }); |
| } |
| |
| |
| 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'); |
| } |
| }); |
| |
| |
| 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; |
| } |
| }); |
| |
| |
| 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; |
| } |
| |
| |
| 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'); |
| } |
| }); |
| }); |
| |
| |
| 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'); |
| |
| |
| const numCandles = 100; |
| for (let i = 0; i < numCandles; i++) { |
| if (!state.isExecuting) break; |
| |
| elements.progressBar.style.width = `${(i / numCandles) * 100}%`; |
| |
| |
| await new Promise(resolve => setTimeout(resolve, 50)); |
| } |
| |
| |
| 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) + '٪'; |
| |
| |
| 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); |
| |
| |
| 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(); |
| } |
| |
| |
| 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'); |
| } |
| |
| |
| 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); |
| |
| |
| state.nodes.forEach(n => deleteNode(n.id)); |
| state.nodes = []; |
| state.connections = []; |
| |
| |
| strategy.nodes.forEach(nodeData => { |
| createNode(nodeData.type, nodeData.x, nodeData.y, nodeData.params); |
| }); |
| |
| |
| 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); |
| } |
| |
| |
| document.querySelectorAll('.template-card').forEach(card => { |
| card.addEventListener('click', () => { |
| const templateName = card.dataset.template; |
| loadTemplate(templateName); |
| }); |
| }); |
| |
| function loadTemplate(templateName) { |
| |
| 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'); |
| } |
| } |
| |
| |
| 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); |
| |
| |
| showToast('آزمایشگاه بصری استراتژی آماده است', 'success'); |
| updatePropertiesPanel(); |
| |
| |
| setInterval(() => { |
| document.querySelectorAll('.connection-path').forEach(path => { |
| path.style.opacity = 0.6 + Math.random() * 0.4; |
| }); |
| }, 2000); |
| </script> |
| </body> |
| </html> |
|
|