From 7666f970c0f18388e7dda48f8c3e58fa5f9371a3 Mon Sep 17 00:00:00 2001 From: BTC Bot Date: Wed, 4 Mar 2026 19:41:16 +0100 Subject: [PATCH] feat: add Hurst Bands, strategy panel, signal markers and multiple UI enhancements --- GEMINI.md | 76 ++ check_db.py | 42 + scratch.js | 91 ++ scripts/generate_custom_history.py | 65 + src/TV/HTS.pine | 86 ++ src/api/dashboard.7z | Bin 0 -> 26651 bytes .../dashboard/static/css/indicators-new.css | 726 +++++++++++ src/api/dashboard/static/index.html | 266 ++-- src/api/dashboard/static/js/app.js | 142 +- .../dashboard/static/js/config/timezone.js | 76 ++ src/api/dashboard/static/js/indicators/atr.js | 88 +- src/api/dashboard/static/js/indicators/bb.js | 77 +- src/api/dashboard/static/js/indicators/hts.js | 248 +++- .../dashboard/static/js/indicators/hurst.js | 179 +++ .../dashboard/static/js/indicators/index.js | 76 +- .../dashboard/static/js/indicators/macd.js | 133 +- .../static/js/indicators/moving_average.js | 221 ++++ src/api/dashboard/static/js/indicators/rsi.js | 142 +- .../dashboard/static/js/indicators/stoch.js | 101 +- src/api/dashboard/static/js/ui/chart.js | 373 +++++- .../dashboard/static/js/ui/hts-visualizer.js | 231 ++++ src/api/dashboard/static/js/ui/index.js | 23 - .../static/js/ui/indicators-panel-new.js | 1157 +++++++++++++++++ .../static/js/ui/indicators-panel-new.js.bak | 868 +++++++++++++ .../static/js/ui/indicators-panel.js | 97 +- src/api/dashboard/static/js/ui/sidebar.js | 56 +- .../dashboard/static/js/ui/signal-markers.js | 228 ++++ .../static/js/ui/signals-calculator.js | 364 ++++++ src/api/dashboard/static/js/ui/simulation.js | 21 +- .../dashboard/static/js/ui/strategy-panel.js | 791 +++++++++++ 30 files changed, 6671 insertions(+), 373 deletions(-) create mode 100644 GEMINI.md create mode 100644 check_db.py create mode 100644 scratch.js create mode 100644 scripts/generate_custom_history.py create mode 100644 src/TV/HTS.pine create mode 100644 src/api/dashboard.7z create mode 100644 src/api/dashboard/static/css/indicators-new.css create mode 100644 src/api/dashboard/static/js/config/timezone.js create mode 100644 src/api/dashboard/static/js/indicators/hurst.js create mode 100644 src/api/dashboard/static/js/indicators/moving_average.js create mode 100644 src/api/dashboard/static/js/ui/hts-visualizer.js create mode 100644 src/api/dashboard/static/js/ui/indicators-panel-new.js create mode 100644 src/api/dashboard/static/js/ui/indicators-panel-new.js.bak create mode 100644 src/api/dashboard/static/js/ui/signal-markers.js create mode 100644 src/api/dashboard/static/js/ui/signals-calculator.js create mode 100644 src/api/dashboard/static/js/ui/strategy-panel.js diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..376df81 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,76 @@ +# BTC Accumulation Bot - Context & Guidelines + +High-performance crypto data collection and trading system for cbBTC on Hyperliquid, optimized for Synology NAS deployment. + +## Project Overview + +- **Purpose**: Collect 1-minute candle data for cbBTC-PERP from Hyperliquid, compute technical indicators on custom timeframes (e.g., 37m, 148m), and execute accumulation strategies. +- **Tech Stack**: + - **Backend**: Python 3.11+ (FastAPI, asyncio, websockets, asyncpg, pandas, numpy). + - **Database**: TimescaleDB (PostgreSQL 15 extension) on Synology DS218+. + - **Infrastructure**: Docker Compose (Network Mode: host for performance). + - **Frontend**: Vanilla JS dashboard for real-time monitoring and charts. +- **Architecture**: + - `DataCollector`: Main orchestrator managing WebSocket ingestion, buffering, and database writes. + - `IndicatorEngine`: Computes technical indicators (MA44, MA125) on multiple timeframes. + - `Brain`: Decision engine that evaluates signals based on indicator state. + - `CustomTimeframeGenerator`: Dynamically generates non-standard intervals from 1m base candles. + +## Building and Running + +### Development & Deployment +- **Deployment**: Run `chmod +x scripts/deploy.sh && ./scripts/deploy.sh` to scaffold directories and start Docker services. +- **Service Control**: + - Start: `cd docker && docker-compose up -d` + - Stop: `cd docker && docker-compose down` + - Logs: `docker-compose logs -f [service_name]` (e.g., `data_collector`, `api_server`, `timescaledb`) +- **Backups**: `scripts/backup.sh` (manages PostgreSQL dumps with 7-day retention). + +### Local API Server (Hybrid Setup) +To run the dashboard/API locally while the database remains on the NAS: +1. **NAS Config**: Ensure `5433:5432` mapping is active in `docker-compose.yml`. +2. **Local Environment**: Create a `.env` file locally: + ```env + DB_HOST=NAS_IP_ADDRESS + DB_PORT=5433 + DB_NAME=btc_data + DB_USER=btc_bot + DB_PASSWORD=YOUR_PASSWORD + ``` +3. **Run Locally**: + ```bash + pip install -r requirements.txt + python -m uvicorn src.api.server:app --host 0.0.0.0 --port 8000 --reload + ``` + +### Testing +- **Manual Verification**: + - API Health: `curl http://localhost:8000/api/v1/health` + - DB Status: `docker exec btc_timescale pg_isready -U btc_bot` +- **Indicator Testing**: `scripts/test_ma44_performance.py` + +## Development Conventions + +### Coding Standards +- **Modularity**: Keep files small (< 500 lines) and focused on a single responsibility. +- **Async First**: Use `asyncio` for all I/O bound operations (WebSockets, Database, API). +- **Type Safety**: Use Pydantic models for configuration and API responses. +- **Logging**: Use structured logging with console prefixes: `[SYSTEM]`, `[CLP]`, `[HEDGE]`, `[MONITOR]`. +- **Golden Rule (Data)**: Always read the source of truth from the blockchain or primary API (Hyperliquid) rather than relying on local calculations if possible. +- **Golden Rule (Positioning)**: CLP positions must use symmetric grid snapping. + +### Database Design +- **Hypertables**: Use TimescaleDB hypertables for `candles` and `indicators` with weekly partitioning. +- **Compression**: Automatic compression is enabled for data older than 7 days to save space on NAS. +- **Gaps**: Continuous gap detection and automatic backfill from Hyperliquid REST API. + +### Dashboard & API +- **FastAPI**: Main entry point in `src/api/server.py`. +- **Static Assets**: Dashboard located in `src/api/dashboard/static/`. +- **Indicator Engine**: JS-side mirror of indicators for the UI chart located in `src/api/dashboard/static/js/indicators/`. + +## Project Roadmap +- **Phase 1 (Complete)**: Data collection, TimescaleDB integration, Basic API. +- **Phase 2 (In-Progress)**: Indicator Engine (SMA, EMA, RSI, MACD), Brain decision logic. +- **Phase 3 (TODO)**: Web3 execution layer (Uniswap V3 swaps on Base). +- **Phase 4 (TODO)**: Aave V3 integration for yield on collected cbBTC. diff --git a/check_db.py b/check_db.py new file mode 100644 index 0000000..b386930 --- /dev/null +++ b/check_db.py @@ -0,0 +1,42 @@ + +import asyncio +import os +from datetime import datetime +import asyncpg + +DB_HOST = os.getenv('DB_HOST', 'localhost') +DB_PORT = int(os.getenv('DB_PORT', 5432)) +DB_NAME = os.getenv('DB_NAME', 'btc_data') +DB_USER = os.getenv('DB_USER', 'btc_bot') +DB_PASSWORD = os.getenv('DB_PASSWORD', '') + +async def check_data(): + conn = await asyncpg.connect( + host=DB_HOST, + port=DB_PORT, + database=DB_NAME, + user=DB_USER, + password=DB_PASSWORD + ) + + try: + print("Checking candle counts...") + rows = await conn.fetch(""" + SELECT interval, COUNT(*), MIN(time), MAX(time) + FROM candles + GROUP BY interval + ORDER BY interval + """) + + for row in rows: + print(f"Interval: {row['interval']}") + print(f" Count: {row['count']}") + print(f" Min Time: {row['min']}") + print(f" Max Time: {row['max']}") + print("-" * 20) + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(check_data()) diff --git a/scratch.js b/scratch.js new file mode 100644 index 0000000..1f42e56 --- /dev/null +++ b/scratch.js @@ -0,0 +1,91 @@ +import { BaseIndicator } from './base.js'; + +export class RSIIndicator extends BaseIndicator { + calculate(candles) { + const period = this.params.period || 14; + + // 1. Calculate RSI using RMA (Wilder's Smoothing) + let rsiValues = new Array(candles.length).fill(null); + let upSum = 0; + let downSum = 0; + const rmaAlpha = 1 / period; + + for (let i = 1; i < candles.length; i++) { + const diff = candles[i].close - candles[i-1].close; + const up = diff > 0 ? diff : 0; + const down = diff < 0 ? -diff : 0; + + if (i < period) { + upSum += up; + downSum += down; + } else if (i === period) { + upSum += up; + downSum += down; + const avgUp = upSum / period; + const avgDown = downSum / period; + rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown))); + upSum = avgUp; // Store for next RMA step + downSum = avgDown; + } else { + upSum = (up - upSum) * rmaAlpha + upSum; + downSum = (down - downSum) * rmaAlpha + downSum; + rsiValues[i] = downSum === 0 ? 100 : (upSum === 0 ? 0 : 100 - (100 / (1 + upSum / downSum))); + } + } + + // Combine results + return rsiValues.map((rsi, i) => { + return { + paneBg: 100, // Background lightening trick + rsi: rsi, + upperBand: 70, + lowerBand: 30 + }; + }); + } + + getMetadata() { + const plots = [ + // Background lightness trick (spans from 0 to 100 constantly) + // Making it darker by reducing opacity since the main background is dark. + { id: 'paneBg', type: 'baseline', baseValue: 0, color: 'transparent', + topLineColor: 'transparent', bottomLineColor: 'transparent', + topFillColor1: 'rgba(255, 255, 255, 0.015)', topFillColor2: 'rgba(255, 255, 255, 0.015)', + bottomFillColor1: 'transparent', bottomFillColor2: 'transparent', + title: '', lastValueVisible: false, width: 0 }, + + // Overbought Gradient Fill (> 70) + { id: 'rsi', type: 'baseline', baseValue: 70, color: 'transparent', + topLineColor: 'transparent', bottomLineColor: 'transparent', + topFillColor1: 'rgba(244, 67, 54, 0.8)', topFillColor2: 'rgba(244, 67, 54, 0.2)', + bottomFillColor1: 'transparent', bottomFillColor2: 'transparent', + title: '', lastValueVisible: false, width: 0 }, + + // Oversold Gradient Fill (< 30) + { id: 'rsi', type: 'baseline', baseValue: 30, color: 'transparent', + topLineColor: 'transparent', bottomLineColor: 'transparent', + topFillColor1: 'transparent', topFillColor2: 'transparent', + bottomFillColor1: 'rgba(76, 175, 80, 0.2)', bottomFillColor2: 'rgba(76, 175, 80, 0.8)', + title: '', lastValueVisible: false, width: 0 }, + + // RSI Line + { id: 'rsi', color: '#7E57C2', title: '', width: 1, lastValueVisible: true }, + + // Bands + { id: 'upperBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }, + { id: 'lowerBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false } + ]; + + return { + name: 'RSI', + description: 'Relative Strength Index', + inputs: [ + { name: 'period', label: 'RSI Length', type: 'number', default: 14, min: 1, max: 100 } + ], + plots: plots, + displayMode: 'pane', + paneMin: 0, + paneMax: 100 + }; + } +} diff --git a/scripts/generate_custom_history.py b/scripts/generate_custom_history.py new file mode 100644 index 0000000..141c7ef --- /dev/null +++ b/scripts/generate_custom_history.py @@ -0,0 +1,65 @@ + +import asyncio +import logging +import os +import sys + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +from src.data_collector.database import DatabaseManager +from src.data_collector.custom_timeframe_generator import CustomTimeframeGenerator + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +async def main(): + logger.info("Starting custom timeframe generation...") + + # DB connection settings from env or defaults + db_host = os.getenv('DB_HOST', 'localhost') + db_port = int(os.getenv('DB_PORT', 5432)) + db_name = os.getenv('DB_NAME', 'btc_data') + db_user = os.getenv('DB_USER', 'btc_bot') + db_password = os.getenv('DB_PASSWORD', '') + + db = DatabaseManager( + host=db_host, + port=db_port, + database=db_name, + user=db_user, + password=db_password + ) + + await db.connect() + + try: + generator = CustomTimeframeGenerator(db) + await generator.initialize() + + # Generate 37m from 1m + logger.info("Generating 37m candles from 1m data...") + count_37m = await generator.generate_historical('37m') + logger.info(f"Generated {count_37m} candles for 37m") + + # Generate 148m from 37m + # Note: 148m generation relies on 37m data existing + logger.info("Generating 148m candles from 37m data...") + count_148m = await generator.generate_historical('148m') + logger.info(f"Generated {count_148m} candles for 148m") + + logger.info("Done!") + + except Exception as e: + logger.error(f"Error generating custom timeframes: {e}") + import traceback + traceback.print_exc() + finally: + await db.disconnect() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/TV/HTS.pine b/src/TV/HTS.pine new file mode 100644 index 0000000..0470626 --- /dev/null +++ b/src/TV/HTS.pine @@ -0,0 +1,86 @@ +//@version=5 +indicator(title='HTS p1otek (Fixed)', overlay=true ) + +// Helper function to return the correct timeframe string for request.security +// Note: We let Pine Script infer the return type to avoid syntax errors +getAutoTFString(chartTFInMinutes) => + float autoTFMinutes = chartTFInMinutes / 4.0 + + // Use an existing time resolution string if possible (D, W, M) + if timeframe.isdaily + // 'D' timeframe is 1440 minutes. 1440 / 4 = 360 minutes (6 hours) + // We return "360" which Pine Script accepts as a resolution + str.tostring(math.round(autoTFMinutes)) + else if timeframe.isweekly or timeframe.ismonthly + // Cannot divide W or M timeframes reliably, return current timeframe string + timeframe.period + else + // For standard minute timeframes, use the calculated minutes + str.tostring(math.round(autoTFMinutes)) + +// Inputs +// FIXED: Changed input.integer to input.int +short = input.int(33, "fast") +long = input.int(144, "slow") +auto = input.bool(false, title = "auto HTS (timeframe/4)") +draw_1h = input.bool(false, title = "draw 1h slow HTS") + +metoda = input.string(title = "type average", defval = "RMA", options=["RMA", "EMA", "SMA", "WMA", "VWMA"]) + +// Calculate chart TF in minutes +float chartTFInMinutes = timeframe.in_seconds() / 60 +// Get the auto-calculated timeframe string +string autoTFString = getAutoTFString(chartTFInMinutes) + + +srednia(src, length, type) => + switch type + "RMA" => ta.rma(src, length) + "EMA" => ta.ema(src, length) + "SMA" => ta.sma(src, length) + "WMA" => ta.wma(src, length) + "VWMA" => ta.vwma(src, length) + +// === Non-Auto (Current Timeframe) Calculations === +string currentTFString = timeframe.period + +shortl = request.security(syminfo.tickerid, currentTFString, srednia(low, short, metoda)) +shorth = request.security(syminfo.tickerid, currentTFString, srednia(high, short, metoda)) +longl = request.security(syminfo.tickerid, currentTFString, srednia(low, long, metoda)) +longh = request.security(syminfo.tickerid, currentTFString, srednia(high, long, metoda)) + +// === Auto Timeframe Calculations === +shortl_auto = request.security(syminfo.tickerid, autoTFString, srednia(low, short, metoda)) +shorth_auto = request.security(syminfo.tickerid, autoTFString, srednia(high, short, metoda)) +longl_auto = request.security(syminfo.tickerid, autoTFString, srednia(low, long, metoda)) +longh_auto = request.security(syminfo.tickerid, autoTFString, srednia(high, long, metoda)) + +// === 1H Timeframe Calculations === +// Use a fixed '60' for 1 hour +longl_1h = request.security(syminfo.tickerid, "60", srednia(low, long, metoda)) +longh_1h = request.security(syminfo.tickerid, "60", srednia(high, long, metoda)) + + +// === Plotting === + +// Auto HTS +plot(auto ? shortl_auto: na, color=color.new(color.aqua, 0), linewidth=1, title="fast low auto") +plot(auto ? shorth_auto: na, color=color.new(color.aqua, 0), linewidth=1, title="fast high auto") +plot(auto ? longl_auto: na, color=color.new(color.red, 0), linewidth=1, title="slow low auto") +plot(auto ? longh_auto: na, color=color.new(color.red, 0), linewidth=1, title="slow high auto") + +// Current TF (only when Auto is enabled, for reference) +ll = plot( auto ? longl: na, color=color.new(color.red, 80), linewidth=1, title="current slow low") +oo = plot( auto ? longh: na, color=color.new(color.red, 80), linewidth=1, title="current slow high") +fill(ll,oo, color=color.new(color.red, 90)) + +// 1H Zone +zone_1hl = plot( draw_1h ? longl_1h: na, color=color.new(color.red, 80), linewidth=1, title="1h slow low") +zone_1hh = plot( draw_1h ? longh_1h: na, color=color.new(color.red, 80), linewidth=1, title="1h slow high") +fill(zone_1hl,zone_1hh, color=color.new(color.red, 90)) + +// Non-Auto HTS +plot(not auto ? shortl: na, color=color.new(color.aqua, 0), linewidth=1, title="fast low") +plot(not auto ? shorth: na, color=color.new(color.aqua, 0), linewidth=1, title="fast high") +plot(not auto ? longl: na, color=color.new(color.red, 0), linewidth=1, title="slow low") +plot(not auto ? longh: na, color=color.new(color.red, 0), linewidth=1, title="slow high") \ No newline at end of file diff --git a/src/api/dashboard.7z b/src/api/dashboard.7z new file mode 100644 index 0000000000000000000000000000000000000000..d698f5f87f8ce7668bde4f4f882bd4c2b72c1ecf GIT binary patch literal 26651 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

p1A(N=FT(efA7tF@&Z!uTd2R}Lwr7_i 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 literal 0 HcmV?d00001 diff --git a/src/api/dashboard/static/css/indicators-new.css b/src/api/dashboard/static/css/indicators-new.css new file mode 100644 index 0000000..a67d85d --- /dev/null +++ b/src/api/dashboard/static/css/indicators-new.css @@ -0,0 +1,726 @@ +/* ============================================================================ + 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; + gap: 5px; +} +.section-title button.clear-all, +.section-title button.visibility-toggle { + display: none; +} +.section-title:hover button.clear-all, +.section-title:hover button.visibility-toggle { + display: inline-block; +} +.visibility-toggle, +.clear-all { + background: var(--tv-red); + border: none; + color: white; + font-size: 9px; + padding: 2px 8px; + border-radius: 3px; + cursor: pointer; +} +.visibility-toggle { + background: var(--tv-blue); +} +.visibility-toggle:hover, +.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 { + font-size: 11px; + color: var(--tv-text-secondary); + margin-left: 8px; +} + +.indicator-actions { + display: flex; + gap: 4px; + margin-left: auto; +} + +.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: inline; + font-size: 11px; + color: var(--tv-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 120px; + } +} + +/* 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; +} + +/* Sidebar Tabs */ +.sidebar-tabs { + display: flex; + gap: 4px; + flex: 1; + margin-right: 8px; +} + +.sidebar-tab { + flex: 1; + background: transparent; + border: none; + color: var(--tv-text-secondary); + font-size: 11px; + padding: 6px 8px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.sidebar-tab:hover { + background: var(--tv-hover); + color: var(--tv-text); +} + +.sidebar-tab.active { + background: rgba(41, 98, 255, 0.15); + color: var(--tv-blue); + font-weight: 600; +} + +/* Sidebar Tab Panels */ +.sidebar-tab-panel { + display: none; + animation: fadeIn 0.2s ease; +} + +.sidebar-tab-panel.active { + display: block; +} + +/* Collapsed sidebar adjustments */ +.right-sidebar.collapsed .sidebar-tabs { + display: none; +} + +/* Strategy Panel Styles */ +.indicator-checklist { + max-height: 120px; + overflow-y: auto; + background: var(--tv-bg); + border: 1px solid var(--tv-border); + border-radius: 4px; + padding: 4px; + margin-top: 4px; +} +.indicator-checklist::-webkit-scrollbar { + width: 4px; +} +.indicator-checklist::-webkit-scrollbar-thumb { + background: var(--tv-border); + border-radius: 2px; +} + +.checklist-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + border-radius: 3px; +} +.checklist-item:hover { + background: var(--tv-hover); +} +.checklist-item input { + cursor: pointer; +} + +.equity-chart-container { + width: 100%; + height: 150px; + margin-top: 12px; + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--tv-border); + background: var(--tv-bg); +} + +.results-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.chart-toggle-group { + display: flex; + background: var(--tv-hover); + border-radius: 4px; + padding: 2px; +} + +.chart-toggle-group .toggle-btn { + padding: 2px 8px; + font-size: 10px; + border: none; + background: transparent; + color: var(--tv-text-secondary); + cursor: pointer; + border-radius: 3px; + transition: all 0.2s ease; +} + +.chart-toggle-group .toggle-btn.active { + background: var(--tv-border); + color: var(--tv-text); +} + +.chart-toggle-group .toggle-btn:hover:not(.active) { + color: var(--tv-text); +} \ No newline at end of file diff --git a/src/api/dashboard/static/index.html b/src/api/dashboard/static/index.html index e610666..db4d05b 100644 --- a/src/api/dashboard/static/index.html +++ b/src/api/dashboard/static/index.html @@ -523,7 +523,77 @@ z-index: 9999; } - /* Price Scale Controls */ + /* Chart Settings Menu */ + .chart-settings-btn { + width: 20px; + height: 20px; + background: rgba(42, 46, 57, 0.9); + border: 1px solid #363c4e; + color: #d1d4dc; + font-size: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + transition: all 0.2s; + padding: 0; + } + + .chart-settings-btn:hover { + background: #363c4e; + border-color: #4a4f5e; + } + + /* Settings Popup */ + .chart-settings-popup { + position: absolute; + right: 0; + top: 26px; + background: #1e222d; + border: 1px solid #363c4e; + border-radius: 6px; + padding: 12px; + z-index: 100; + min-width: 200px; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); + display: none; + } + + .chart-settings-popup.show { + display: block; + } + + .settings-group { + margin-bottom: 12px; + } + + .settings-group:last-child { + margin-bottom: 0; + } + + .settings-label { + display: block; + font-size: 11px; + color: #787b86; + margin-bottom: 4px; + text-transform: uppercase; + } + + .settings-select { + width: 100%; + background: #131722; + border: 1px solid #363c4e; + color: #d1d4dc; + padding: 6px 8px; + border-radius: 4px; + font-size: 12px; + } + + .settings-select:focus { + outline: none; + border-color: #2962ff; + } .price-scale-controls { position: absolute; right: 10px; @@ -715,13 +785,16 @@ margin-top: 4px; } - .ta-signal { +.ta-signal { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; - margin-top: 8px; + margin-top: 4px; + min-width: 60px; + text-align: center; + font-family: 'Courier New', monospace; } .ta-signal.buy { @@ -1270,12 +1343,47 @@ } } - @media (max-width: 600px) { +@media (max-width: 600px) { .ta-content { grid-template-columns: 1fr; } } - + + /* Signal Styles */ + .ta-summary-badge { + background: var(--tv-bg); + border: 1px solid var(--tv-border); + padding: 4px 10px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + } + + .ta-summary-badge.buy { + background: rgba(38, 166, 154, 0.2); + color: var(--tv-green); + border-color: var(--tv-green); + } + + .ta-summary-badge.sell { + background: rgba(239, 83, 80, 0.2); + color: var(--tv-red); + border-color: var(--tv-red); + } + + .ta-summary-badge.hold { + background: rgba(120, 123, 134, 0.2); + color: var(--tv-text-secondary); + border-color: var(--tv-text-secondary); + } + + .signal-strength-fill { + height: 100%; + transition: width 0.3s ease; + } + +

@@ -1317,8 +1425,26 @@
+ +
+
+ + +
+
-
-
Loading technical analysis...
+
+
Waiting for candle data...
- -