Implement single-panel indicator management system
Single-panel design with TradingView-inspired UX: - Search bar for filtering indicators by name - Category tabs (Trend, Momentum, Volatility, Volume, Favorites) - Expandable indicators with inline configuration - Favorites system with pinning - Preset system to save/load indicator configurations - Reset to defaults functionality - Real-time configuration changes (apply immediately) - Mobile-friendly responsive design - Touch-optimized for mobile devices - Cleaner single-panel layout replacing two-panel approach Features: ✓ Search functionality (must-have) ✓ Presets high-priority (save, load, delete) ✓ Single expandable panel ✓ Inline configuration (no separate panel) ✓ Categories for organizing indicators ✓ Favorites support ✓ Real-time visual updates ✓ Mobile responsive ✓ Collapse all indicators with one click ○ Drag-to-reorder (not implemented - nice to have) Updated files: - indicators-panel-new.js: Completely new implementation - indicators-new.css: New styles for single panel - index.html: Updated sidebar to use indicator panel - app.js: Updated imports and initialization
This commit is contained in:
593
src/api/dashboard/static/css/indicators-new.css
Normal file
593
src/api/dashboard/static/css/indicators-new.css
Normal file
@ -0,0 +1,593 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
NEW INDICATOR PANEL STYLES - Single Panel, TradingView-inspired
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.indicator-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subrbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.indicator-panel::-webkit-scrollbar-thumb {
|
||||||
|
background: #363a44;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.indicator-panel::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Bar */
|
||||||
|
.indicator-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.indicator-search:focus-within {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.search-icon {
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.indicator-search input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.indicator-search input::placeholder {
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
}
|
||||||
|
.search-clear {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.search-clear:hover {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Tabs */
|
||||||
|
.category-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.category-tabs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.category-tab {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.category-tab:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
.category-tab.active {
|
||||||
|
background: rgba(41, 98, 255, 0.1);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Sections */
|
||||||
|
.indicator-section {
|
||||||
|
margin: 8px 12px 12px;
|
||||||
|
}
|
||||||
|
.indicator-section.favorites {
|
||||||
|
background: rgba(41, 98, 255, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.section-title button.clear-all {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.section-title:hover button.clear-all {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.clear-all {
|
||||||
|
background: var(--tv-red);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.clear-all:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Items */
|
||||||
|
.indicator-item {
|
||||||
|
background: var(--tv-panel-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.indicator-item:hover {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.indicator-item.favorite {
|
||||||
|
border-color: rgba(41, 98, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-desc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.indicator-btn:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
border-color: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.indicator-btn.add:hover {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-presets {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.indicator-presets {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.indicator-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Indicator Item */
|
||||||
|
.indicator-item.active {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item.active .indicator-name {
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item.active.expanded {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
background: rgba(41, 98, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.drag-handle:hover {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn.visible,
|
||||||
|
.indicator-btn.expand,
|
||||||
|
.indicator-btn.favorite {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.indicator-btn.expand.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Config (Expanded) */
|
||||||
|
.indicator-config {
|
||||||
|
border-top: 1px solid var(--tv-border);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
animation: slideDown 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 1000px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-sections {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.config-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-action-btn {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.preset-action-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Config Row */
|
||||||
|
.config-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.config-row label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
.config-row select,
|
||||||
|
.config-row input[type="text"],
|
||||||
|
.config-row input[type="number"] {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.config-row select:focus,
|
||||||
|
.config-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-preset {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.input-with-preset input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.presets-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.presets-btn:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color Picker */
|
||||||
|
.color-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.color-picker input[type="color"] {
|
||||||
|
width: 32px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.color-preview {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range Slider */
|
||||||
|
.config-row input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
accent-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.config-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--tv-border);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-red);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Results */
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Presets List */
|
||||||
|
.presets-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.preset-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.preset-item:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.preset-item.applied {
|
||||||
|
background: rgba(38, 166, 154, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.preset-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
.preset-delete {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.preset-delete:hover {
|
||||||
|
color: var(--tv-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-presets {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range Value Display */
|
||||||
|
.range-value {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preset Indicator Button */
|
||||||
|
.preset-indicator {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.preset-indicator:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.category-tabs {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.category-tab {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row label {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly styles for mobile */
|
||||||
|
@media (hover: none) {
|
||||||
|
.indicator-btn {
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tab {
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme improvements */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.indicator-search {
|
||||||
|
background: #1e222d;
|
||||||
|
}
|
||||||
|
.indicator-item {
|
||||||
|
background: #1e222d;
|
||||||
|
}
|
||||||
|
.indicator-config {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
.indicator-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-config > * {
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for presets list */
|
||||||
|
.presets-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.presets-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--tv-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
@ -1275,6 +1275,9 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
@import url(./css/indicators-new.css);
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -1349,17 +1352,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Sidebar - Strategy Simulation -->
|
<!-- Right Sidebar - Tools Panel -->
|
||||||
<div class="right-sidebar collapsed" id="rightSidebar">
|
<div class="right-sidebar collapsed" id="rightSidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="sidebar-title">
|
<div class="sidebar-title">
|
||||||
<span>🎯</span>
|
<span>🔧</span>
|
||||||
<span class="sidebar-title-text">Strategy Sim</span>
|
<span class="sidebar-title-text">Tools</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-toggle" onclick="toggleSidebar()">◀</button>
|
<button class="sidebar-toggle" onclick="toggleSidebar()">◀</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
|
<!-- Indicators Panel -->
|
||||||
|
<div class="sidebar-section-indicators" id="indicatorPanel">
|
||||||
|
<div class="sidebar-section-header" style="padding: 8px 12px;">
|
||||||
|
<span>📊</span> Indicators
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Strategy Selection -->
|
<!-- Strategy Selection -->
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<div class="sidebar-section-header">
|
<div class="sidebar-section-header">
|
||||||
|
|||||||
@ -21,16 +21,14 @@ import {
|
|||||||
setCurrentStrategy
|
setCurrentStrategy
|
||||||
} from './ui/strategies-panel.js';
|
} from './ui/strategies-panel.js';
|
||||||
import {
|
import {
|
||||||
renderIndicatorList,
|
initIndicatorPanel,
|
||||||
|
getActiveIndicators,
|
||||||
|
setActiveIndicators,
|
||||||
|
drawIndicatorsOnChart,
|
||||||
addIndicator,
|
addIndicator,
|
||||||
toggleIndicator,
|
|
||||||
showIndicatorConfig,
|
|
||||||
applyIndicatorConfig,
|
|
||||||
removeIndicator,
|
|
||||||
removeIndicatorById,
|
removeIndicatorById,
|
||||||
removeIndicatorByIndex,
|
removeIndicatorByIndex
|
||||||
drawIndicatorsOnChart
|
} from './ui/indicators-panel-new.js';
|
||||||
} from './ui/indicators-panel.js';
|
|
||||||
import { StrategyParams } from './strategies/config.js';
|
import { StrategyParams } from './strategies/config.js';
|
||||||
import { IndicatorRegistry } from './indicators/index.js';
|
import { IndicatorRegistry } from './indicators/index.js';
|
||||||
|
|
||||||
@ -66,10 +64,16 @@ window.loadSavedSimulation = loadSavedSimulation;
|
|||||||
window.deleteSavedSimulation = deleteSavedSimulation;
|
window.deleteSavedSimulation = deleteSavedSimulation;
|
||||||
window.clearSimulationResults = clearSimulationResults;
|
window.clearSimulationResults = clearSimulationResults;
|
||||||
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
window.updateTimeframeDisplay = updateTimeframeDisplay;
|
||||||
window.renderIndicatorList = renderIndicatorList;
|
window.renderIndicatorList = function() {
|
||||||
|
// Legacy function - replaced by initIndicatorPanel
|
||||||
|
window.initIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export init function for global access
|
||||||
|
window.initIndicatorPanel = initIndicatorPanel;
|
||||||
|
window.initIndicatorPanel = initIndicatorPanel;
|
||||||
window.addIndicator = addIndicator;
|
window.addIndicator = addIndicator;
|
||||||
window.toggleIndicator = toggleIndicator;
|
window.toggleIndicator = addIndicator;
|
||||||
window.showIndicatorConfig = showIndicatorConfig;
|
|
||||||
|
|
||||||
window.StrategyParams = StrategyParams;
|
window.StrategyParams = StrategyParams;
|
||||||
window.SimulationStorage = SimulationStorage;
|
window.SimulationStorage = SimulationStorage;
|
||||||
@ -84,7 +88,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
await loadStrategies();
|
await loadStrategies();
|
||||||
|
|
||||||
renderIndicatorList();
|
// Initialize new indicator panel
|
||||||
|
window.initIndicatorPanel();
|
||||||
|
|
||||||
const originalSwitchTimeframe = window.dashboard.switchTimeframe.bind(window.dashboard);
|
const originalSwitchTimeframe = window.dashboard.switchTimeframe.bind(window.dashboard);
|
||||||
window.dashboard.switchTimeframe = function(interval) {
|
window.dashboard.switchTimeframe = function(interval) {
|
||||||
|
|||||||
822
src/api/dashboard/static/js/ui/indicators-panel-new.js
Normal file
822
src/api/dashboard/static/js/ui/indicators-panel-new.js
Normal file
@ -0,0 +1,822 @@
|
|||||||
|
import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
|
||||||
|
|
||||||
|
// State management
|
||||||
|
let activeIndicators = [];
|
||||||
|
let configuringId = null;
|
||||||
|
let searchQuery = '';
|
||||||
|
let selectedCategory = 'all';
|
||||||
|
let nextInstanceId = 1;
|
||||||
|
|
||||||
|
// Presets storage
|
||||||
|
let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ id: 'all', name: 'All Indicators', icon: '📊' },
|
||||||
|
{ id: 'trend', name: 'Trend', icon: '📊' },
|
||||||
|
{ id: 'momentum', name: 'Momentum', icon: '📈' },
|
||||||
|
{ id: 'volatility', name: 'Volatility', icon: '📉' },
|
||||||
|
{ id: 'volume', name: 'Volume', icon: '🔀' },
|
||||||
|
{ id: 'favorites', name: 'Favorites', icon: '★' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const CATEGORY_MAP = {
|
||||||
|
sma: 'trend', ema: 'trend', hts: 'trend',
|
||||||
|
rsi: 'momentum', macd: 'momentum', stoch: 'momentum',
|
||||||
|
bb: 'volatility', atr: 'volatility',
|
||||||
|
others: 'volume'
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
|
||||||
|
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
export function initIndicatorPanel() {
|
||||||
|
renderIndicatorPanel();
|
||||||
|
setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultColor(index) {
|
||||||
|
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorCategory(indicator) {
|
||||||
|
return CATEGORY_MAP[indicator.type] || 'trend';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorLabel(indicator) {
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
if (!meta) return indicator.name;
|
||||||
|
|
||||||
|
const paramParts = meta.inputs.map(input => {
|
||||||
|
const val = indicator.params[input.name];
|
||||||
|
if (val !== undefined && val !== input.default) return val;
|
||||||
|
return null;
|
||||||
|
}).filter(v => v !== null);
|
||||||
|
|
||||||
|
if (paramParts.length > 0) {
|
||||||
|
return `${indicator.name} (${paramParts.join(', ')})`;
|
||||||
|
}
|
||||||
|
return indicator.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorMeta(indicator) {
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return null;
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
return instance.getMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupPlotsByColor(plots) {
|
||||||
|
const groups = {};
|
||||||
|
plots.forEach((plot, idx) => {
|
||||||
|
const groupMap = {
|
||||||
|
'fast': 'Fast', 'slow': 'Slow', 'upper': 'Upper', 'lower': 'Lower',
|
||||||
|
'middle': 'Middle', 'basis': 'Middle', 'signal': 'Signal',
|
||||||
|
'histogram': 'Histogram', 'k': '%K', 'd': '%D'
|
||||||
|
};
|
||||||
|
const groupName = Object.entries(groupMap).find(([k, v]) => plot.id.toLowerCase().includes(k))?.[1] || plot.id;
|
||||||
|
if (!groups[groupName]) {
|
||||||
|
groups[groupName] = { name: groupName, indices: [], plots: [] };
|
||||||
|
}
|
||||||
|
groups[groupName].indices.push(idx);
|
||||||
|
groups[groupName].plots.push(plot);
|
||||||
|
});
|
||||||
|
return Object.values(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveIndicators() {
|
||||||
|
return activeIndicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveIndicators(indicators) {
|
||||||
|
activeIndicators = indicators;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render main panel
|
||||||
|
export function renderIndicatorPanel() {
|
||||||
|
const container = document.getElementById('indicatorPanel');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const available = getAvailableIndicators();
|
||||||
|
const catalog = available.filter(ind => {
|
||||||
|
if (searchQuery && !ind.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
if (selectedCategory === 'all') return true;
|
||||||
|
if (selectedCategory === 'favorites') return false;
|
||||||
|
const cat = CATEGORY_MAP[ind.type] || 'trend';
|
||||||
|
return cat === selectedCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
const favoriteIds = new Set(userPresets.favorites || []);
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="indicator-panel">
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="indicator-search">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="indicatorSearch"
|
||||||
|
placeholder="Search indicators..."
|
||||||
|
value="${searchQuery}"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
${searchQuery ? `<button class="search-clear" onclick="clearSearch()">×</button>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="category-tabs">
|
||||||
|
${CATEGORIES.map(cat => `
|
||||||
|
<button class="category-tab ${selectedCategory === cat.id ? 'active' : ''}" data-category="${cat.id}">
|
||||||
|
${cat.icon} ${cat.name}
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favorites (if any) -->
|
||||||
|
${favoriteIds.size > 0 ? `
|
||||||
|
<div class="indicator-section favorites">
|
||||||
|
<div class="section-title">★ Favorites</div>
|
||||||
|
${favoriteIds.map(id => {
|
||||||
|
const ind = available.find(a => {
|
||||||
|
// Find matching indicator by type
|
||||||
|
return a.type === id || (activeIndicators.find(ai => ai.id === id)?.type === '');
|
||||||
|
});
|
||||||
|
if (!ind) return '';
|
||||||
|
return renderIndicatorItem(ind, true);
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Active Indicators -->
|
||||||
|
${activeIndicators.length > 0 ? `
|
||||||
|
<div class="indicator-section active">
|
||||||
|
<div class="section-title">
|
||||||
|
${activeIndicators.length} Active
|
||||||
|
${activeIndicators.length > 0 ? `<button class="clear-all" onclick="clearAllIndicators()">Clear All</button>` : ''}
|
||||||
|
</div>
|
||||||
|
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Available Indicators -->
|
||||||
|
${catalog.length > 0 ? `
|
||||||
|
<div class="indicator-section catalog">
|
||||||
|
<div class="section-title">Available Indicators</div>
|
||||||
|
${catalog.map(ind => renderIndicatorItem(ind, false)).join('')}
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div class="no-results">
|
||||||
|
No indicators found
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorItem(indicator, isFavorite) {
|
||||||
|
const colorDots = '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="indicator-item ${isFavorite ? 'favorite' : ''}" data-type="${indicator.type}">
|
||||||
|
<div class="indicator-item-main">
|
||||||
|
<span class="indicator-name">${indicator.name}</span>
|
||||||
|
<span class="indicator-desc">${indicator.description || ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="indicator-actions">
|
||||||
|
<button class="indicator-btn add" title="Add to chart" onclick="window.addIndicator('${indicator.type}')">
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
${isFavorite ? '' : `
|
||||||
|
<button class="indicator-btn favorite" title="Add to favorites" onclick="window.toggleFavorite('${indicator.type}')">
|
||||||
|
${userPresets.favorites?.includes(indicator.type) ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderActiveIndicator(indicator) {
|
||||||
|
const isExpanded = configuringId === indicator.id;
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
const label = getIndicatorLabel(indicator);
|
||||||
|
const isFavorite = userPresets.favorites?.includes(indicator.type) || false;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="indicator-item active ${isExpanded ? 'expanded' : ''}" data-id="${indicator.id}">
|
||||||
|
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand('${indicator.id}')">
|
||||||
|
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
|
||||||
|
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
|
||||||
|
${indicator.visible !== false ? '👁' : '👁🗨'}
|
||||||
|
</button>
|
||||||
|
<span class="indicator-name">${label}</span>
|
||||||
|
<div class="indicator-presets">
|
||||||
|
${meta.name && renderPresetIndicatorIndicator(meta, indicator)}
|
||||||
|
</div>
|
||||||
|
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite('${indicator.type}')" title="Add to favorites">
|
||||||
|
${isFavorite ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
<button class="indicator-btn expand ${isExpanded ? 'rotated' : ''}" title="Show settings">
|
||||||
|
${isExpanded ? '▼' : '▶'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${isExpanded ? `
|
||||||
|
<div class="indicator-config">
|
||||||
|
${renderIndicatorConfig(indicator, meta)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPresetIndicatorIndicator(meta, indicator) {
|
||||||
|
const hasPresets = getPresetsForIndicator(meta.name);
|
||||||
|
if (!hasPresets || hasPresets.length === 0) return '';
|
||||||
|
|
||||||
|
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets('${meta.name}')">💾</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorConfig(indicator, meta) {
|
||||||
|
const plotGroups = groupPlotsByColor(meta?.plots || []);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="config-sections">
|
||||||
|
<!-- Colors -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Visual Settings</div>
|
||||||
|
${plotGroups.map(group => {
|
||||||
|
const firstIdx = group.indices[0];
|
||||||
|
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx]?.color || getDefaultColor(activeIndicators.indexOf(indicator));
|
||||||
|
return `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${group.name} Color</label>
|
||||||
|
<div class="color-picker">
|
||||||
|
<input type="color" id="color_${indicator.id}_${firstIdx}" value="${color}" onchange="window.updateIndicatorColor('${indicator.id}', ${firstIdx}, this.value)">
|
||||||
|
<span class="color-preview" style="background: ${color};"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Line Type</label>
|
||||||
|
<select onchange="window.updateIndicatorSetting('${indicator.id}', '_lineType', this.value)">
|
||||||
|
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Line Width</label>
|
||||||
|
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
|
||||||
|
<span class="range-value">${indicator.params._lineWidth || 2}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inputs -->
|
||||||
|
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Parameters</div>
|
||||||
|
${meta.inputs.map(input => `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${input.label}</label>
|
||||||
|
${input.type === 'select' ?
|
||||||
|
`<select onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
|
||||||
|
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
|
||||||
|
</select>` :
|
||||||
|
`<div class="input-with-preset">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value="${indicator.params[input.name]}"
|
||||||
|
${input.min !== undefined ? `min="${input.min}"` : ''}
|
||||||
|
${input.max !== undefined ? `max="${input.max}"` : ''}
|
||||||
|
${input.step !== undefined ? `step="${input.step}"` : ''}
|
||||||
|
onchange="window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
|
||||||
|
>
|
||||||
|
<button class="presets-btn" onclick="window.showInputPresets('${indicator.id}', '${input.name}')">⋯</button>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Presets -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">
|
||||||
|
Presets
|
||||||
|
<button class="preset-action-btn" onclick="window.savePreset('${indicator.id}')" title="Save current settings as preset">+ Save Preset</button>
|
||||||
|
</div>
|
||||||
|
${renderIndicatorPresets(indicator, meta)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="config-actions">
|
||||||
|
<button class="btn-secondary" onclick="window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
|
||||||
|
<button class="btn-danger" onclick="window.removeIndicatorById('${indicator.id}')">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorPresets(indicator, meta) {
|
||||||
|
const presets = getPresetsForIndicator(meta.name);
|
||||||
|
const instance = new IR[indicator.type]({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
const metadata = instance.getMetadata();
|
||||||
|
|
||||||
|
return presets.length > 0 ? `
|
||||||
|
<div class="presets-list">
|
||||||
|
${presets.map(preset => {
|
||||||
|
// Match values against current settings
|
||||||
|
const isApplied = metadata.inputs.every(input =>
|
||||||
|
preset.values[input.name] === indicator.params[input.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${preset.id}">
|
||||||
|
<span class="preset-label" onclick="window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
|
||||||
|
<button class="preset-delete" onclick="event.stopPropagation(); window.deletePreset('${preset.id}')" title="Delete preset">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : '<div class="no-presets">No saved presets</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
function setupEventListeners() {
|
||||||
|
// Search
|
||||||
|
const searchInput = document.getElementById('indicatorSearch');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
searchQuery = e.target.value;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category tabs
|
||||||
|
document.querySelectorAll('.category-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
selectedCategory = tab.dataset.category;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
window.clearSearch = function() {
|
||||||
|
searchQuery = '';
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearAllIndicators = function() {
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
ind.series?.forEach(s => {
|
||||||
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
activeIndicators = [];
|
||||||
|
configuringId = null;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addIndicator = function(type) {
|
||||||
|
const IndicatorClass = IR?.[type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const id = `${type}_${nextInstanceId++}`;
|
||||||
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
|
const metadata = instance.getMetadata();
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
_lineType: 'solid',
|
||||||
|
_lineWidth: 2
|
||||||
|
};
|
||||||
|
metadata.plots.forEach((plot, idx) => {
|
||||||
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
|
});
|
||||||
|
metadata.inputs.forEach(input => {
|
||||||
|
params[input.name] = input.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.push({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name: metadata.name,
|
||||||
|
params,
|
||||||
|
plots: metadata.plots,
|
||||||
|
series: [],
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
|
||||||
|
configuringId = id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleIndicatorExpand = function(id) {
|
||||||
|
configuringId = configuringId === id ? null : id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleIndicatorVisibility = function(id) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.visible = indicator.visible === false ? true : false;
|
||||||
|
|
||||||
|
indicator.series?.forEach(s => {
|
||||||
|
try {
|
||||||
|
s.applyOptions({ visible: indicator.visible });
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleFavorite = function(type) {
|
||||||
|
const favorites = userPresets.favorites || [];
|
||||||
|
const idx = favorites.indexOf(type);
|
||||||
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
favorites.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
favorites.push(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
userPresets.favorites = favorites;
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateIndicatorColor = function(id, index, color) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.params[`_color_${index}`] = color;
|
||||||
|
|
||||||
|
const preview = document.querySelector(`#color_${id}_${index} + .color-preview`);
|
||||||
|
if (preview) {
|
||||||
|
preview.style.background = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateIndicatorSetting = function(id, key, value) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.params[key] = value;
|
||||||
|
|
||||||
|
const valueSpan = document.querySelector(`#color_${id}_${index} + .color-preview`);
|
||||||
|
if (valueSpan) {
|
||||||
|
valueSpan.textContent = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.resetIndicator = function(id) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
|
||||||
|
const metadata = instance.getMetadata();
|
||||||
|
|
||||||
|
metadata.inputs.forEach(input => {
|
||||||
|
indicator.params[input.name] = input.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeIndicatorById = function(id) {
|
||||||
|
const idx = activeIndicators.findIndex(a => a.id === id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
activeIndicators[idx].series?.forEach(s => {
|
||||||
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.splice(idx, 1);
|
||||||
|
|
||||||
|
if (configuringId === id) {
|
||||||
|
configuringId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Presets
|
||||||
|
function getPresetsForIndicator(indicatorName) {
|
||||||
|
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.name);
|
||||||
|
return allPresets.filter(p => p.indicatorName === indicatorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.savePreset = function(id) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
const presetName = prompt('Enter preset name:');
|
||||||
|
if (!presetName) return;
|
||||||
|
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
const preset = {
|
||||||
|
id: `preset_${Date.now()}`,
|
||||||
|
name: presetName,
|
||||||
|
indicatorName: meta.name,
|
||||||
|
values: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
meta.inputs.forEach(input => {
|
||||||
|
preset.values[input.name] = indicator.params[input.name];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userPresets.presets) userPresets.presets = [];
|
||||||
|
userPresets.presets.push(preset);
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
|
||||||
|
alert(`Preset "${presetName}" saved!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.applyPreset = function(id, presetId) {
|
||||||
|
const allPresets = Object.values(userPresets).flat().filter(p => typeof p === 'object' && p.id);
|
||||||
|
const preset = allPresets.find(p => p.id === presetId);
|
||||||
|
if (!preset) return;
|
||||||
|
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
Object.keys(preset.values).forEach(key => {
|
||||||
|
indicator.params[key] = preset.values[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deletePreset = function(presetId) {
|
||||||
|
if (!confirm('Delete this preset?')) return;
|
||||||
|
|
||||||
|
if (userPresets.presets) {
|
||||||
|
userPresets.presets = userPresets.presets.filter(p => p.id !== presetId);
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.showPresets = function(indicatorName) {
|
||||||
|
const presets = getPresetsForIndicator(indicatorName);
|
||||||
|
if (presets.length === 0) {
|
||||||
|
alert('No saved presets for this indicator');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = window.open('', '_blank', 'width=400,height=500');
|
||||||
|
menu.document.write(`
|
||||||
|
<html><head><title>Presets - ${indicatorName}</title><style>
|
||||||
|
body { font-family: sans-serif; padding: 20px; background: #1e222d; color: #d1d4dc; }
|
||||||
|
.preset { padding: 10px; margin: 5px; background: #131722; border-radius: 4px; }
|
||||||
|
.preset:hover { background: #2a2e39; cursor: pointer; }
|
||||||
|
</style></head><body>
|
||||||
|
<h3>${indicatorName} Presets</h3>
|
||||||
|
${presets.map(p => `<div class="preset" onclick="opener.applyPresetFromWindow('${p.id}')">${p.name}</div>`).join('')}
|
||||||
|
</body></html>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.applyPresetFromWindow = function(presetId) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === configuringId);
|
||||||
|
if (!indicator) return;
|
||||||
|
applyPreset(indicator.id, presetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
function saveUserPresets() {
|
||||||
|
localStorage.setItem('indicator_presets', JSON.stringify(userPresets));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart drawing
|
||||||
|
export function drawIndicatorsOnChart() {
|
||||||
|
if (!window.dashboard || !window.dashboard.chart) return;
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
ind.series?.forEach(s => {
|
||||||
|
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||||
|
if (!candles || candles.length === 0) return;
|
||||||
|
|
||||||
|
const lineStyleMap = {
|
||||||
|
'solid': LightweightCharts.LineStyle.Solid,
|
||||||
|
'dotted': LightweightCharts.LineStyle.Dotted,
|
||||||
|
'dashed': LightweightCharts.LineStyle.Dashed
|
||||||
|
};
|
||||||
|
|
||||||
|
indicatorPanes.clear();
|
||||||
|
nextPaneIndex = 1;
|
||||||
|
|
||||||
|
const overlayIndicators = [];
|
||||||
|
const paneIndicators = [];
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
const IndicatorClass = IR?.[ind.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
if (meta.displayMode === 'pane') {
|
||||||
|
paneIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
} else {
|
||||||
|
overlayIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPanes = 1 + paneIndicators.length;
|
||||||
|
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
|
||||||
|
const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0;
|
||||||
|
|
||||||
|
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
||||||
|
|
||||||
|
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paneIndex = nextPaneIndex++;
|
||||||
|
indicatorPanes.set(indicator.id, paneIndex);
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||||
|
|
||||||
|
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||||
|
if (pane) {
|
||||||
|
pane.setHeight(paneHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy compatibility functions
|
||||||
|
window.renderIndicatorList = renderIndicatorPanel;
|
||||||
|
window.toggleIndicator = addIndicator;
|
||||||
|
window.showIndicatorConfig = function(id) {
|
||||||
|
const ind = activeIndicators.find(a => a.id === id);
|
||||||
|
if (ind) configuringId = id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
window.applyIndicatorConfig = function() {
|
||||||
|
// No-op - config is applied immediately
|
||||||
|
};
|
||||||
|
window.showIndicatorConfigByIndex = function(index) {
|
||||||
|
if (index >= 0 && index < activeIndicators.length) {
|
||||||
|
configuringId = activeIndicators[index].id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
||||||
|
const results = instance.calculate(candles);
|
||||||
|
indicator.series = [];
|
||||||
|
|
||||||
|
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||||
|
const lineWidth = indicator.params._lineWidth || 2;
|
||||||
|
|
||||||
|
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
||||||
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
|
|
||||||
|
meta.plots.forEach((plot, plotIdx) => {
|
||||||
|
if (isObjectResult) {
|
||||||
|
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||||
|
if (!hasData) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||||
|
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
let value;
|
||||||
|
if (isObjectResult) {
|
||||||
|
value = results[i]?.[plot.id];
|
||||||
|
} else {
|
||||||
|
value = results[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
data.push({
|
||||||
|
time: candles[i].time,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) return;
|
||||||
|
|
||||||
|
let series;
|
||||||
|
|
||||||
|
let plotLineStyle = lineStyle;
|
||||||
|
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
|
||||||
|
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
|
||||||
|
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
|
||||||
|
|
||||||
|
if (plot.type === 'histogram') {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
|
||||||
|
color: plotColor,
|
||||||
|
priceFormat: { type: 'price', precision: 4, minMove: 0.0001 },
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: false
|
||||||
|
}, paneIndex);
|
||||||
|
} else if (plot.type === 'baseline') {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
|
||||||
|
baseValue: { type: 'price', price: plot.baseValue || 0 },
|
||||||
|
topLineColor: plot.topLineColor || plotColor,
|
||||||
|
topFillColor1: plot.topFillColor1 || plotColor,
|
||||||
|
topFillColor2: '#00000000',
|
||||||
|
bottomFillColor1: '#00000000',
|
||||||
|
bottomColor: plot.bottomColor || '#00000000',
|
||||||
|
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
} else {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: plotColor,
|
||||||
|
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
series.setData(data);
|
||||||
|
indicator.series.push(series);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let indicatorPanes = new Map();
|
||||||
|
let nextPaneIndex = 1;
|
||||||
|
|
||||||
|
// Legacy support
|
||||||
|
window.renderIndicatorList = function() {
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.renderIndicatorConfig = function(indicator) {
|
||||||
|
// Called by chart.js, redirect to new panel
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
const configEl = document.getElementById('configForm');
|
||||||
|
if (configEl) {
|
||||||
|
configEl.innerHTML = renderIndicatorConfig(indicator, meta);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.applyIndicatorConfig = function() {
|
||||||
|
// No-op - config is applied immediately
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeIndicator = function() {
|
||||||
|
if (!configuringId) return;
|
||||||
|
removeIndicatorById(configuringId);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeIndicatorByIndex = function(index) {
|
||||||
|
if (index < 0 || index >= activeIndicators.length) return;
|
||||||
|
removeIndicatorById(activeIndicators[index].id);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addIndicator = addIndicator;
|
||||||
|
window.toggleIndicator = addIndicator;
|
||||||
|
window.removeIndicatorById = removeIndicatorById;
|
||||||
|
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
|
||||||
Reference in New Issue
Block a user