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 0000000..d698f5f Binary files /dev/null and b/src/api/dashboard.7z differ 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; + } + +