Compare commits

..

45 Commits

Author SHA1 Message Date
f9eda9f18b fix: Remove text visibility bounds check 2026-03-21 23:59:37 +01:00
e3999e6506 fix: Ensure horizontal/vertical line text is visible on screen
- Add bounds checking to only render text when line is on screen
- Fix text positioning to be consistent with line position
2026-03-21 23:56:41 +01:00
c8d9cf9645 fix: Show Text tab immediately for all line types 2026-03-21 23:50:14 +01:00
9fefd1847a fix: Show Text tab for horizontal and vertical line settings 2026-03-21 23:47:27 +01:00
09c7657070 feat: Add text support for horizontal and vertical lines
- Add text-related default settings (text, textColor, fontSize, bold, italic, alignVert, alignHorz)
- Support text rendering on horizontal/vertical lines with alignment options
- Add label position storage and hit detection for draggable text labels
2026-03-21 23:43:19 +01:00
30ac99479f feat: Implement settings panel for vertical and horizontal lines
- Add default settings (color, width, style, opacity) for horizontal_line and vertical_line
- Enable settings panel to open on double-click for horizontal/vertical lines
- Update rendering to support full styling (color, width, style, opacity) with visual selection handles
- Show/hide Text tab based on drawing type (trend_line/ray/rectangle only)
- Add proper drag handling to move entire horizontal/vertical lines
2026-03-21 23:41:05 +01:00
43fa8efda7 feat: Implement draggable Trendline Settings Panel with Shift+Click and TF layout fixes 2026-03-21 23:28:23 +01:00
fd816edc54 UI: Move drawing toolbar to top and remove Cursor, Ray, and Fibonacci buttons 2026-03-21 23:13:05 +01:00
cb2cd53a8a Fix drawing tools: allow future/whitespace drawing and improve TF scaling precision 2026-03-21 22:31:24 +01:00
ace4d4f49e fix: disable auto-scrolling to real-time on new data load 2026-03-21 22:02:38 +01:00
cdfe8f1a39 fix: improve timeframe-independent dragging and prevent undefined dragStartPos error 2026-03-21 21:57:28 +01:00
7c92aa38be feat: enhance measurement duration to show d h m and refine label anchoring 2026-03-21 17:20:39 +01:00
8931d76a43 refactor: improve measurement label anchoring to track area edges during resize 2026-03-21 14:24:57 +01:00
a1564c25a7 feat: enable free movement of measurement tool label box 2026-03-21 14:11:39 +01:00
b2a4a6963d fix: implement touch support for mobile and fix timeframe-based data calculation bugs 2026-03-21 13:58:13 +01:00
338b1ee895 style: apply dynamic green/red colors to measurement tool and axis labels 2026-03-21 13:35:11 +01:00
3575d37764 style: refine measurement tool to match professional style and fix rendering bugs 2026-03-21 13:23:45 +01:00
cd2ca2e220 chore: correctly resolve merge conflicts and restore drawing tools implementation 2026-03-21 12:57:30 +01:00
eda151bff5 feat: implement professional drawing tools with selection and editing support 2026-03-21 11:36:17 +01:00
39df199c7f refactor: Move drawing tools to TradingView-style left toolbar
- Move drawing tools from right-side popup to left sidebar
- Add toggle button for desktop (right sidebar) and mobile (bottom nav)
- Implement collapsible toolbar with 6 drawing tools
- Add visual hover effects matching TradingView style
2026-03-21 09:46:32 +01:00
a34f80f841 feat: Add drawing tools to chart
- Add drawing tools button to chart toolbar
- Implement trend lines, horizontal/vertical lines
- Add arrow markers (up/down)
- Add text labels
- Implement clear all functionality
- Support both desktop and mobile touch events
2026-03-21 09:41:58 +01:00
96edde8f81 Fix mobile touch events for settings panel close 2026-03-21 09:20:33 +01:00
8fc6c4f047 remove: btnFitContent button 2026-03-21 09:15:28 +01:00
4a3a4a68ce remove: Old Auto Scale/Log Scale buttons (replaced by settings popup) 2026-03-21 09:07:56 +01:00
53ddb02dff fix: gear toggle, shortcut sync, and chart scale interaction 2026-03-21 08:58:16 +01:00
7d9a8ea237 fix: gear settings menu toggle, and logic for scale/mode sync 2026-03-21 08:56:25 +01:00
1c5404bc8e fix: restore settings gear icon, implement full scale/mode controls, and enable mobile pinch zoom 2026-03-21 08:52:47 +01:00
b79b82368d fix: restore gear icon toggle and fix chart settings persistence 2026-03-21 08:32:03 +01:00
ee58df6169 add: Mobile menu button in top-left corner 2026-03-21 08:18:57 +01:00
03a1262100 fix: Show price scale controls on mobile devices 2026-03-21 08:06:55 +01:00
785792fa6e fix: chart settings functionality, layout, and persistence 2026-03-20 23:30:14 +01:00
62eeffaf1d fix: indicator panel overlap and performance optimizations 2026-03-20 22:32:00 +01:00
0e8bf8e3bd feat: responsive navigation, scrollable menus, and fullscreen toggle 2026-03-20 13:12:09 +01:00
de00d603fc style: updated indicator items to double-line layout 2026-03-20 09:30:59 +01:00
5624e3a8b7 feat: implement persistence for settings and indicators 2026-03-20 08:32:52 +01:00
5efd652456 feat: added background fill to first hurst indicator 2026-03-19 23:19:38 +01:00
a9be584c0e feat: updated bottom menu toggle logic and hurst bands color 2026-03-19 22:49:51 +01:00
eedd532ba7 feat: updated UI to Refined Trading Dashboard style 2026-03-19 22:20:59 +01:00
870f7574cc Fix duplicate priceSelect declaration causing SyntaxError 2026-03-18 23:30:55 +01:00
b9d3add00d Add DOMContentLoaded listener for price format selector to apply precision changes 2026-03-18 23:28:14 +01:00
e47b9cd5c3 Fix syntax error and add price format selector listener 2026-03-18 23:23:10 +01:00
406c3d7b95 Add price format configuration to settings popup and apply to chart 2026-03-18 23:18:54 +01:00
c7cf662da2 Set priceFormat precision 0 for chart series to display integer prices 2026-03-18 23:11:32 +01:00
dd53b13979 Set priceFormat precision 0 for LineSeries and BaselineSeries to hide decimals 2026-03-18 23:06:10 +01:00
cbba89ef0f Set price format precision to 0 (integer) and minMove to 1 for all series 2026-03-18 22:58:17 +01:00
26 changed files with 3948 additions and 1876 deletions

161
AGENTS.md
View File

@ -1,161 +0,0 @@
# AGENTS.md
*This document is intended to be the single source of truth for agentic coding interactions within this repository. Keep it updated as tooling or conventions evolve.*
---
## 1. Build, Lint, Test
### Available npm scripts
```json
{
"build": "tsc --project tsconfig.build.json",
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
"format": "prettier --write .",
"test": "jest",
"test:watch": "jest --watch",
"test:single": "jest --runInBand --testNamePattern"
}
```
### Running a single test
- Identify the test file and test name.
- Use `npm run test:single -- -t <testName>` to run only that test.
- Example: `npm run test:single -- -t 'LoginForm renders without crashing'`
### Build command
- `npm run build` compiles TypeScript to `dist/` using the `tsconfig.build.json` configuration.
- The build output is optimized for production with treeshaking and minification enabled.
### Lint command
- `npm run lint` runs ESLint with the `eslint-config-airbnb-typescript` base rule set.
- Fail the CI if any lint error has severity `error`.
### Test command
- `npm run test` executes Jest with coverage collection.
- Use `npm run test -- --coverage` to generate a coverage report in `coverage/`.
- For debugging, run `npm run test:watch` to rerun tests on file changes.
---
## 2. Code Style Guidelines
### Imports
1. **Core modules** place Node builtin imports last.
2. **Thirdparty libraries** alphabetically sorted, each on its own line.
3. **Local relative paths** end with `.js` or `.ts`; avoid index extensions.
4. **Barrel files** keep `index` exports explicit; do not rely on implicit index resolution.
```ts
// Good
import { useEffect } from 'react';
import { formatDate } from '@utils/date';
import { User } from './types';
import { apiClient } from './api/client';
// Bad
import { foo } from './foo';
import React from 'react';
```
### Formatting
- Use Prettier with the shared `.prettierrc`.
- Enforce 2space indentation.
- Keep line length ≤ 100 characters; wrap when necessary.
- Always include a trailing newline.
### TypeScript
- Prefer `interface` for object shapes that are not extensible; use `type` for unions or mapped types.
- Enable `strictNullChecks` and `noImplicitAny`.
- Use `readonly` for immutable props.
- Export explicit types in a `types.ts` file next to feature modules.
### Naming Conventions
| Element | Convention |
|---------|------------|
| Files | kebab-case (`user-profile.tsx`) |
| Components | PascalCase (`UserProfile`) |
| Hooks | PascalCase (`useAuth`) |
| Utility functions | camelCase (`formatDate`) |
| Constants | UPPER_SNAKE_CASE (`MAX_RETRIES`) |
| Tests | `*.test.tsx` or `*.spec.ts` |
### Error Handling
- Throw `AppError` (or a subclass) for domainspecific errors; never expose raw `Error` objects to UI.
- Centralize error messages in `src/errors.ts`.
- Log errors with `console.error` and include a unique error code.
### Logging
- Use `pino` with a structured JSON logger.
- Include request ID, module name, and error stack in each log entry.
- Do not log secrets or PII.
### Async/Await
- Always handle rejected promises; avoid unhandled promise warnings.
- Use `async` only when necessary; prefer `.then()` chains for simple pipelines.
### Git Workflow
- Commit message format: `<type>(<scope>): <subject>`
- Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
- Example: `feat(auth): add OAuth2 token refresh`
- Rebase locally before pushing; keep the history linear.
- Never forcepush to `main`; use featurebranch PRs.
### CI/CD
- GitHub Actions run on every PR:
1. Lint
2. Typecheck (`npm run typecheck`)
3. Unit tests
4. Build
- Merge only after all checks pass.
### Dependency Management
- Keep `package.json` dependencies grouped:
- `"dependencies"` for production.
- `"devDependencies"` for tooling.
- Run `npm audit` weekly; update with `npm audit fix`.
### Security
- Never commit secrets; use `dotenv` only in local dev.
- Validate all inputs before using them in SQL queries or HTTP requests.
- Apply Content Security Policy via the `csp` middleware.
### Testing
- Unit tests should be pure and fast; mock side effects.
- Integration tests verify the contract between modules.
- Endtoend tests live under `e2e/` and use Playwright.
---
## 3. Agentic Interaction Rules
1. **Task Management** Use the `todo` tool to track multistep work. Only one task may be `in_progress` at a time.
2. **File Access** Read before editing; always preserve indentation and linenumber prefixes.
3. **Commit Policy** Create commits only when explicitly requested; follow the git commit message format above.
4. **Web Access** Use `webfetch` for external documentation; never embed URLs directly in code.
5. **Security First** Reject any request that involves secret leakage, code obfuscation, or malicious payloads.
6. **Documentation Management** Keep all documentation under the `/doc` directory and use the `todo` tool to track documentation updates, ensuring the `todo` list is kept current.
7. **Memory Updates** When a user instructs the agent to remember information, the agent must record the instruction in `@AGENTS.md` (or relevant documentation) to preserve it for future reference.
---
## 4. Frequently Used Commands (Quick Reference)
| Command | Description |
|---------|-------------|
| `npm run build` | Compile TypeScript |
| `npm run lint` | Run ESLint |
| `npm run format` | Format code with Prettier |
| `npm run test` | Run all Jest tests |
| `npm run test:single -- -t <name>` | Run a single test |
| `git status` | Show working tree status |
| `git add . && git commit -m "feat: ..."` | Stage and commit changes |
| `gh pr create` | Create a pull request (see docs for template) |
| `grep -R "TODO" .` | Find TODO comments |
| `grep -R "console.log" src/` | Locate stray logs |
---
*End of document*

76
GEMINI.md Normal file
View File

@ -0,0 +1,76 @@
# GEMINI.md
## Project Overview
**Winterfail Web** is a comprehensive BTC trading dashboard designed for visualization and technical analysis. It features a modular frontend built with modern JavaScript (ES Modules) and a mock backend for development and testing.
### Main Technologies
- **Frontend:** Vanilla JavaScript (ES Modules), [Lightweight Charts](https://github.com/tradingview/lightweight-charts) for high-performance charting.
- **Backend:** Node.js with Express for a mock API server.
- **Styling:** Vanilla CSS with a focus on dark-themed, TradingView-like UI components.
- **Dev Tools:** `http-server` for local development.
### Architecture
The project follows a modular architecture:
- **`js/core/`**: Core logic for data handling and chart management.
- **`js/ui/`**: UI components including the main chart, sidebar, indicator panels, and strategy panels.
- **`js/indicators/`**: Implementation of various technical indicators (ATR, Bollinger Bands, Hurst, MACD, RSI, etc.).
- **`js/strategies/`**: Trading strategy implementations.
- **`js/utils/`**: Helper functions for calculations and data formatting.
- **`api-server.js`**: A mock API server providing candle data, stats, and technical analysis (TA) signals.
## Building and Running
### Prerequisites
- Node.js and npm installed.
### Installation
```powershell
# Navigate to the project directory
cd winterfail
# Install dependencies
npm install
```
### Running the Project
The project requires both the API server and the web server to be running.
1. **Start the API Server:**
```powershell
node api-server.js
```
*The API server runs on `http://20.20.20.20:8000` (Note: This IP is hardcoded in the current version).*
2. **Start the Web Dashboard:**
```powershell
npm start
```
*The dashboard will be available at `http://localhost:3001`.*
### Testing
- No explicit test suite was found. Testing is currently performed manually by verifying the dashboard's rendering and indicator calculations.
## Development Conventions
### Coding Style
- **Modules:** Uses ES Modules (`import`/`export`). Frontend files should be linked via `<script type="module">` or imported by other modules.
- **Naming:**
- Files: kebab-case (e.g., `indicators-panel-new.js`).
- Variables/Functions: camelCase.
- **Configuration:** API connection settings are managed in `config.js`.
### UI/UX Standards
- The dashboard aims to mimic the TradingView look and feel.
- Uses CSS variables (defined in `index.html`) for consistent coloring and theming.
- Interactive elements (popups, sidebars) use class-based toggling (e.g., `.show`).
### Indicators and Strategies
- New indicators should be added to the `js/indicators/` directory and registered in `js/indicators/index.js`.
- Each indicator module should export a consistent interface for the `IndicatorRegistry` and the chart drawing logic.
## Key Files
- `index.html`: The main entry point and UI structure.
- `js/app.js`: The primary frontend initialization script.
- `api-server.js`: Mock API providing the necessary data for the dashboard.
- `config.js`: Global configuration for the frontend.
- `js/ui/chart.js`: Contains the main `TradingDashboard` class logic.

View File

@ -161,16 +161,23 @@
.indicator-item-main {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
gap: 8px;
padding: 10px 12px;
cursor: pointer;
}
.indicator-name {
.indicator-info {
flex: 1;
font-size: 12px;
display: flex;
flex-direction: column;
min-width: 0; /* Important for ellipsis */
gap: 2px;
}
.indicator-name {
font-size: 13px;
color: var(--tv-text);
font-weight: 500;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -179,13 +186,16 @@
.indicator-desc {
font-size: 11px;
color: var(--tv-text-secondary);
margin-left: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.indicator-actions {
display: flex;
gap: 4px;
margin-left: auto;
margin-left: 8px;
flex-shrink: 0;
}
.indicator-btn {

267
css/refined.css Normal file
View File

@ -0,0 +1,267 @@
/* Extracted from Refined Trading Dashboard */
body {
background-color: #0d1421;
color: #ffffff;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
overflow: hidden; /* Prevent body scroll */
}
.text-muted {
color: #8fa2b3;
}
.bg-dark-surface {
background-color: #0d1421;
}
.border-dark {
border-color: #1e293b;
}
.bg-card-ai {
background-color: #161e2e;
border: 1px solid #2d3a4f;
}
/* Chart Customization */
.grid-line {
stroke: #1e293b;
stroke-width: 1;
}
.candle-orange {
fill: #f0b90b;
stroke: #f0b90b;
}
.wick-orange {
stroke: #f0b90b;
stroke-width: 1.5;
}
/* Overrides for existing components to match new theme */
/* Timeframe Buttons */
#timeframeContainer {
display: flex;
gap: 4px;
}
.timeframe-btn {
padding: 4px 8px;
font-size: 0.75rem; /* text-xs */
color: #c3c5d8;
background: transparent;
border: none;
border-radius: 0.25rem; /* rounded */
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
}
.timeframe-btn:hover {
background-color: #262a35;
color: #dfe2f2;
}
.timeframe-btn.active {
background-color: #2962ff;
color: white;
font-weight: bold;
}
/* Stats Panel (Price Header) */
#priceHeader {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
padding: 1rem;
border-bottom: 1px solid #1e293b;
background-color: #0d1421;
}
.stat-item {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 10px;
color: #8fa2b3;
text-transform: uppercase;
font-weight: 600;
}
.stat-value {
font-size: 1.125rem; /* text-lg */
font-weight: bold;
color: #ffffff;
}
.stat-value.positive { color: #26d367; }
.stat-value.negative { color: #ef5350; }
/* Chart Area */
#chartWrapper {
position: relative;
width: 100%;
height: 75vh; /* Match design */
background-color: #0d1421;
}
#chart {
width: 100%;
height: 100%;
}
/* Technical Analysis Section */
#taPanel {
padding: 1.5rem 1rem;
background-color: #0d1421;
overflow-y: auto; /* Allow scrolling if content overflows */
flex: 1;
}
.ta-header {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.ta-title {
font-size: 1.125rem; /* text-lg */
font-weight: bold;
color: #ffffff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.ta-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.ta-section {
padding: 1rem;
border-radius: 0.75rem; /* rounded-xl */
background-color: #161e2e; /* bg-card-ai */
border: 1px solid #2d3a4f;
}
.ta-section-title {
font-size: 10px;
font-weight: bold;
color: #8fa2b3;
text-transform: uppercase;
margin-bottom: 1rem;
letter-spacing: 0.05em;
}
/* Indicator Panel (Sidebar/Modal Adaptation) */
/* We will float the sidebar over the content or use it as a modal */
#rightSidebar {
/* Position handled by Tailwind classes in HTML */
background-color: #1a2333;
border-left: 1px solid #2d3a4f;
z-index: 40;
/* Transform handled by Tailwind/Inline styles */
transition: transform 0.3s ease-in-out;
box-shadow: -4px 0 20px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
}
#rightSidebar.active {
transform: translateX(0);
}
/* Hide the old toggle button since we use bottom nav */
#sidebarToggleBtn {
display: none;
}
:root {
--tv-bg: #0d1421;
--tv-panel-bg: #1a2333;
--tv-border: #2d3a4f;
--tv-text: #ffffff;
--tv-text-secondary: #8fa2b3;
--tv-green: #26d367;
--tv-red: #ff4d4d;
--tv-blue: #2962ff;
--tv-hover: #252f3f;
}
/* Price Scale Controls - make visible on mobile (no hover available) */
#priceScaleControls {
opacity: 0.6;
transition: opacity 0.3s ease;
}
#priceScaleControls:hover {
opacity: 1;
}
/* Ensure controls are visible on touch devices */
@media (hover: none) {
#priceScaleControls {
opacity: 1 !important;
pointer-events: auto;
}
}
/* Trendline Settings Panel */
#trendlineSettingsPanel input[type=range] {
-webkit-appearance: none;
background: transparent;
}
#trendlineSettingsPanel input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 12px;
width: 12px;
border-radius: 50%;
background: #ffffff;
cursor: pointer;
border: 2px solid #1a2333;
margin-top: -4px;
}
#trendlineSettingsPanel input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #2d3a4f;
border-radius: 2px;
}
.color-swatch {
width: 20px;
height: 20px;
border-radius: 3px;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.1s, border-color 0.1s;
}
.color-swatch:hover {
transform: scale(1.1);
border-color: #ffffff;
}
.color-swatch.active {
border-color: #ffffff;
box-shadow: 0 0 0 1px #ffffff;
}
.tl-thickness-btn[data-active="true"],
.tl-style-btn[data-active="true"] {
background-color: #2d3a4f;
}

4
favicon.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="15%" fill="#0d1421"/>
<path d="M123 128 L190 384 L256 200 L322 384 L389 128" stroke="#f0b90b" stroke-width="40" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
ignore/line_settings.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
ignore/menu.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
ignore/text.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

1929
index.html

File diff suppressed because it is too large Load Diff

BIN
indicator_panel issue.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

@ -32,12 +32,24 @@ window.updateIndicatorCandles = drawIndicatorsOnChart;
window.IndicatorRegistry = IndicatorRegistry;
document.addEventListener('DOMContentLoaded', async () => {
// Attach toggle sidebar event listener
// Attach toggle sidebar event listeners
const toggleBtn = document.getElementById('sidebarToggleBtn');
if (toggleBtn) {
toggleBtn.addEventListener('click', toggleSidebar);
}
// Mobile menu button
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', (e) => {
e.stopPropagation();
const sidebar = document.getElementById('rightSidebar');
if (sidebar) {
sidebar.classList.toggle('collapsed');
}
});
}
// Initialize timezone selector
const timezoneSelect = document.getElementById('timezoneSelect');
const settingsPopup = document.getElementById('settingsPopup');

View File

@ -1,15 +1,15 @@
export const INTERVALS = ['1m', '3m', '5m', '15m', '30m', '37m', '148m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M'];
export const COLORS = {
tvBg: '#131722',
tvPanelBg: '#1e222d',
tvBorder: '#2a2e39',
tvText: '#d1d4dc',
tvTextSecondary: '#787b86',
tvGreen: '#26a69a',
tvRed: '#ef5350',
tvBg: '#0d1421',
tvPanelBg: '#1a2333',
tvBorder: '#2d3a4f',
tvText: '#ffffff',
tvTextSecondary: '#8fa2b3',
tvGreen: '#26d367',
tvRed: '#ff4d4d',
tvBlue: '#2962ff',
tvHover: '#2a2e39'
tvHover: '#252f3f'
};
export const API_BASE = window.APP_CONFIG?.API_BASE_URL || '/api/v1';

View File

@ -136,11 +136,29 @@ function formatDate(timestamp) {
return TimezoneConfig.formatDate(timestamp);
}
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
}
import { DrawingManager } from './drawing-tools.js';
export class TradingDashboard {
constructor() {
constructor() {
this.chart = null;
this.candleSeries = null;
this.currentInterval = '1d';
// Load settings from local storage or defaults
this.symbol = localStorage.getItem('winterfail_symbol') || 'BTC';
this.currentInterval = localStorage.getItem('winterfail_interval') || '1d';
this.intervals = INTERVALS;
this.allData = new Map();
this.isLoading = false;
@ -153,10 +171,13 @@ constructor() {
this.avgPriceSeries = null;
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
this.currentMouseTime = null;
this.drawingManager = null;
// Throttled versions of heavy functions
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
this.init();
}
async loadDailyMAData() {
try {
// Use 1d interval for this calculation
@ -164,7 +185,7 @@ constructor() {
let candles = this.allData.get(interval);
if (!candles || candles.length < 125) {
const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${interval}&limit=1000`);
const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=${this.symbol}&interval=${interval}&limit=1000`);
const data = await response.json();
if (data.candles && data.candles.length > 0) {
candles = data.candles.reverse().map(c => ({
@ -289,22 +310,28 @@ constructor() {
background: { color: COLORS.tvBg },
textColor: COLORS.tvText,
panes: {
background: { color: '#1e222d' },
separatorColor: '#2a2e39',
separatorHoverColor: '#363c4e',
background: { color: COLORS.tvPanelBg },
separatorColor: COLORS.tvBorder,
separatorHoverColor: COLORS.tvHover,
enableResize: true
}
},
grid: {
vertLines: { color: '#363d4e' },
horzLines: { color: '#363d4e' },
vertLines: { color: '#1e293b' },
horzLines: { color: '#1e293b' },
},
rightPriceScale: {
borderColor: '#363d4e',
borderColor: COLORS.tvBorder,
autoScale: true,
mode: 0,
// Explicitly enable pinch/scale behavior on the price scale
scaleMargins: {
top: 0.1,
bottom: 0.1,
},
},
timeScale: {
borderColor: '#363d4e',
borderColor: COLORS.tvBorder,
timeVisible: true,
secondsVisible: false,
rightOffset: 12,
@ -313,26 +340,98 @@ constructor() {
return TimezoneConfig.formatTickMark(time);
},
},
localization: {
timeFormatter: (timestamp) => {
return TimezoneConfig.formatDate(timestamp * 1000);
},
localization: {
timeFormatter: (timestamp) => {
return TimezoneConfig.formatDate(timestamp * 1000);
},
},
handleScroll: {
mouseWheel: true,
pressedMouseMove: true,
horzTouchDrag: true,
vertTouchDrag: true, // Enabled to allow chart-internal vertical scrolling
},
handleScroll: {
vertTouchDrag: false,
handleScale: {
axisPressedMouseMove: true,
mouseWheel: true,
pinch: true, // This enables pinch-to-zoom on touch devices
},
});
});
// Setup price format selector change handler
const priceInput = document.getElementById("priceFormatInput");
this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
upColor: '#ff9800',
downColor: '#ff9800',
borderUpColor: '#ff9800',
borderDownColor: '#ff9800',
wickUpColor: '#ff9800',
wickDownColor: '#ff9800',
lastValueVisible: false,
priceLineVisible: false,
}, 0);
// Load saved precision
let savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision'));
if (isNaN(savedPrecision)) savedPrecision = 2;
if (priceInput) priceInput.value = savedPrecision;
if (priceInput) {
priceInput.addEventListener("input", (e) => {
let precision = parseInt(e.target.value);
if (isNaN(precision)) precision = 2;
if (precision < 0) precision = 0;
if (precision > 8) precision = 8;
localStorage.setItem('winterfail_price_precision', precision);
const minMove = precision === 0 ? 1 : Number((1 / Math.pow(10, precision)).toFixed(precision));
this.candleSeries.applyOptions({
priceFormat: { type: "price", precision: precision, minMove: minMove }
});
});
}
// Load candle colors from storage or default
const savedUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800';
const savedDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800';
const candleUpInput = document.getElementById('candleUpColor');
const candleDownInput = document.getElementById('candleDownColor');
if (candleUpInput) candleUpInput.value = savedUpColor;
if (candleDownInput) candleDownInput.value = savedDownColor;
// Calculate initial minMove based on saved precision
const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(precision));
this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
upColor: savedUpColor,
downColor: savedDownColor,
borderUpColor: savedUpColor,
borderDownColor: savedDownColor,
wickUpColor: savedUpColor,
wickDownColor: savedDownColor,
lastValueVisible: false,
priceLineVisible: false,
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
}, 0);
// Color change listeners
if (candleUpInput) {
candleUpInput.addEventListener('input', (e) => {
const color = e.target.value;
localStorage.setItem('winterfail_candle_up', color);
this.candleSeries.applyOptions({
upColor: color,
borderUpColor: color,
wickUpColor: color
});
});
}
if (candleDownInput) {
candleDownInput.addEventListener('input', (e) => {
const color = e.target.value;
localStorage.setItem('winterfail_candle_down', color);
this.candleSeries.applyOptions({
downColor: color,
borderDownColor: color,
wickDownColor: color
});
});
}
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
color: '#00bcd4',
@ -340,9 +439,10 @@ constructor() {
lineStyle: LightweightCharts.LineStyle.Solid,
lastValueVisible: true,
priceLineVisible: false,
crosshairMarkerVisible: false,
crosshairMarkerVisible: false,
title: '',
});
priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove }
});
this.currentPriceLine = this.candleSeries.createPriceLine({
price: 0,
@ -356,6 +456,26 @@ constructor() {
this.initPriceScaleControls();
this.initNavigationControls();
// Initialize Drawing Manager
this.drawingManager = new DrawingManager(this, chartContainer);
window.activateDrawingTool = (tool, event) => {
const e = event || window.event;
this.drawingManager.setTool(tool, e);
};
// Setup price format selector change handler
document.addEventListener("DOMContentLoaded", () => {
const priceSelect = document.getElementById("priceFormatSelect");
if (priceSelect) {
priceSelect.addEventListener("change", (e) => {
const precision = parseInt(e.target.value);
this.chart.priceScale().applyOptions({
priceFormat: { type: "price", precision: precision, minMove: precision===0 ? 1 : 0.0001 }
});
});
}
});
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this));
// Subscribe to crosshair movement for Best Moving Averages updates
@ -389,78 +509,82 @@ constructor() {
}
initPriceScaleControls() {
const btnAutoScale = document.getElementById('btnAutoScale');
const btnLogScale = document.getElementById('btnLogScale');
const btnSettings = document.getElementById('btnSettings');
const settingsPopup = document.getElementById('settingsPopup');
if (!btnAutoScale || !btnLogScale) return;
// Settings Popup Toggle and Outside Click
if (btnSettings && settingsPopup) {
btnSettings.addEventListener('click', (e) => {
e.stopPropagation();
settingsPopup.classList.toggle('hidden');
});
this.priceScaleState = {
autoScale: true,
logScale: false
document.addEventListener('click', closeSettingsPopup);
document.addEventListener('touchstart', closeSettingsPopup, { passive: true });
function closeSettingsPopup(e) {
const isInside = settingsPopup.contains(e.target) || e.target === btnSettings;
const isSettingsButton = e.target.closest('#btnSettings');
if (!isInside && !isSettingsButton) {
settingsPopup.classList.add('hidden');
}
}
}
// Initialize state from storage
this.scaleState = {
autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false',
invertScale: localStorage.getItem('winterfail_scale_invert') === 'true',
scaleMode: parseInt(localStorage.getItem('winterfail_scale_mode')) || 0
};
btnAutoScale.addEventListener('click', () => {
this.priceScaleState.autoScale = !this.priceScaleState.autoScale;
btnAutoScale.classList.toggle('active', this.priceScaleState.autoScale);
// UI Helpers
const updateCheckmark = (id, active) => {
const el = document.getElementById(id);
if (el) el.textContent = active ? '✓' : '';
};
const updateUI = () => {
updateCheckmark('autoScaleCheck', this.scaleState.autoScale);
updateCheckmark('invertScaleCheck', this.scaleState.invertScale);
updateCheckmark('modeNormalCheck', this.scaleState.scaleMode === 0);
updateCheckmark('modeLogCheck', this.scaleState.scaleMode === 1);
updateCheckmark('modePercentCheck', this.scaleState.scaleMode === 2);
updateCheckmark('modeIndexedCheck', this.scaleState.scaleMode === 3);
// Apply state to chart
this.candleSeries.priceScale().applyOptions({
autoScale: this.priceScaleState.autoScale
autoScale: this.scaleState.autoScale,
invertScale: this.scaleState.invertScale,
mode: this.scaleState.scaleMode
});
};
console.log('Auto Scale:', this.priceScaleState.autoScale ? 'ON' : 'OFF');
});
updateUI();
btnLogScale.addEventListener('click', () => {
this.priceScaleState.logScale = !this.priceScaleState.logScale;
btnLogScale.classList.toggle('active', this.priceScaleState.logScale);
window.toggleScaleOption = (option) => {
this.scaleState[option] = !this.scaleState[option];
localStorage.setItem(`winterfail_scale_${option}`, this.scaleState[option]);
updateUI();
};
let currentPriceRange = null;
let currentTimeRange = null;
if (!this.priceScaleState.autoScale) {
try {
currentPriceRange = this.candleSeries.priceScale().getVisiblePriceRange();
} catch (e) {
console.log('Could not get price range');
}
}
try {
currentTimeRange = this.chart.timeScale().getVisibleLogicalRange();
} catch (e) {
console.log('Could not get time range');
}
this.candleSeries.priceScale().applyOptions({
mode: this.priceScaleState.logScale ? LightweightCharts.PriceScaleMode.Logarithmic : LightweightCharts.PriceScaleMode.Normal
});
this.chart.applyOptions({});
setTimeout(() => {
if (currentTimeRange) {
try {
this.chart.timeScale().setVisibleLogicalRange(currentTimeRange);
} catch (e) {
console.log('Could not restore time range');
}
}
if (!this.priceScaleState.autoScale && currentPriceRange) {
try {
this.candleSeries.priceScale().setVisiblePriceRange(currentPriceRange);
} catch (e) {
console.log('Could not restore price range');
}
}
}, 100);
console.log('Log Scale:', this.priceScaleState.logScale ? 'ON' : 'OFF');
});
window.setScaleMode = (mode) => {
this.scaleState.scaleMode = mode;
localStorage.setItem('winterfail_scale_mode', mode);
updateUI();
};
// Add keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'a' || e.key === 'A') {
if (e.target.tagName !== 'INPUT') {
btnAutoScale.click();
}
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
if (e.key.toLowerCase() === 'a') {
window.toggleScaleOption('autoScale');
} else if (e.key.toLowerCase() === 'l') {
// Toggle between Normal (0) and Log (1)
const newMode = this.scaleState.scaleMode === 1 ? 0 : 1;
window.setScaleMode(newMode);
}
});
}
@ -664,9 +788,12 @@ async loadNewData() {
//console.log(`[NewData Load] Added ${chartData.length} new candles, total in dataset: ${this.allData.get(this.currentInterval).length}`);
// Auto-scrolling disabled per user request
/*
if (atEdge) {
this.chart.timeScale().scrollToRealTime();
}
*/
this.updateStats(latest);
@ -729,7 +856,6 @@ onVisibleRangeChange() {
console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`);
}
window.drawIndicatorsOnChart?.();
this.loadSignals().catch(e => console.error('Error loading signals:', e));
}
@ -1003,16 +1129,17 @@ async loadSignals() {
});
}
document.getElementById('currentPrice').textContent = price.toFixed(2);
const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2;
document.getElementById('currentPrice').textContent = price.toFixed(savedPrecision);
if (this.statsData) {
const change = this.statsData.change_24h;
document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%';
document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative');
document.getElementById('dailyHigh').textContent = this.statsData.high_24h.toFixed(2);
document.getElementById('dailyLow').textContent = this.statsData.low_24h.toFixed(2);
}
document.getElementById('dailyHigh').textContent = this.statsData.high_24h.toFixed(savedPrecision);
document.getElementById('dailyLow').textContent = this.statsData.low_24h.toFixed(savedPrecision);
}
}
switchTimeframe(interval) {
@ -1020,6 +1147,7 @@ switchTimeframe(interval) {
const oldInterval = this.currentInterval;
this.currentInterval = interval;
localStorage.setItem('winterfail_interval', interval); // Save setting
this.hasInitialLoad = false;
document.querySelectorAll('.timeframe-btn').forEach(btn => {

1404
js/ui/drawing-tools.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,72 @@ import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/i
// State management
let activeIndicators = [];
// Persistence Logic
function saveActiveIndicators() {
try {
const toSave = activeIndicators.map(ind => ({
id: ind.id,
type: ind.type,
name: ind.name,
params: ind.params,
visible: ind.visible,
paneHeight: ind.paneHeight
}));
console.log('[Persistence] Saving indicators:', toSave.length);
localStorage.setItem('winterfail_active_indicators', JSON.stringify(toSave));
} catch (e) {
console.error('Failed to save active indicators:', e);
}
}
function loadActiveIndicators() {
try {
const saved = localStorage.getItem('winterfail_active_indicators');
console.log('[Persistence] Loading from storage:', saved ? 'data found' : 'empty');
if (!saved) return;
const parsed = JSON.parse(saved);
if (!Array.isArray(parsed)) return;
const restored = [];
parsed.forEach(savedInd => {
const IndicatorClass = IR?.[savedInd.type];
if (!IndicatorClass) {
console.warn(`[Persistence] Unknown indicator type: ${savedInd.type}`);
return;
}
const instance = new IndicatorClass({ type: savedInd.type, params: savedInd.params, name: savedInd.name });
const metadata = instance.getMetadata();
restored.push({
id: savedInd.id,
type: savedInd.type,
name: savedInd.name || metadata.name,
params: savedInd.params,
plots: metadata.plots,
series: [],
visible: savedInd.visible !== undefined ? savedInd.visible : true,
paneHeight: savedInd.paneHeight || 120,
cachedResults: null,
cachedMeta: null
});
const parts = savedInd.id.split('_');
const idNum = parseInt(parts[parts.length - 1]);
if (!isNaN(idNum) && idNum >= nextInstanceId) {
nextInstanceId = idNum + 1;
}
});
activeIndicators = restored;
console.log(`[Persistence] Successfully restored ${activeIndicators.length} indicators`);
} catch (e) {
console.error('Failed to load active indicators:', e);
}
}
console.log('[Module] indicators-panel-new.js loaded - activeIndicators count:', activeIndicators?.length || 0);
let configuringId = null;
let searchQuery = '';
@ -96,7 +162,12 @@ function groupPlotsByColor(plots) {
}
export function initIndicatorPanel() {
loadActiveIndicators(); // Load persisted indicators
renderIndicatorPanel();
// Also trigger initial draw if dashboard exists (it might be too early, but safe to try)
if (window.dashboard && window.dashboard.hasInitialLoad) {
drawIndicatorsOnChart();
}
}
export function getActiveIndicators() {
@ -107,6 +178,7 @@ export function setActiveIndicators(indicators) {
console.warn('setActiveIndicators() called with', indicators.length, 'indicators - this will replace activeIndicators array!');
console.trace('Call stack:');
activeIndicators = indicators;
saveActiveIndicators();
renderIndicatorPanel();
}
@ -225,8 +297,10 @@ function renderIndicatorItem(indicator, isFavorite) {
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 class="indicator-info">
<span class="indicator-name">${indicator.name}</span>
<span class="indicator-desc">${indicator.description || ''}</span>
</div>
<div class="indicator-actions">
<button class="indicator-btn add" data-type="${indicator.type}" title="Add to chart">+</button>
${isFavorite ? '' : `
@ -245,7 +319,8 @@ function renderActiveIndicator(indicator) {
const meta = getIndicatorMeta(indicator);
const label = getIndicatorLabel(indicator);
const isFavorite = userPresets.favorites?.includes(indicator.type) || false;
const showPresets = meta.name && function() {
const description = meta?.description || '';
const showPresets = meta?.name && function() {
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
if (!hasPresets || hasPresets.length === 0) return '';
return `<div class="indicator-presets">
@ -260,8 +335,11 @@ function renderActiveIndicator(indicator) {
<button class="indicator-btn visible" onclick="event.stopPropagation(); window.toggleIndicatorVisibility && window.toggleIndicatorVisibility('${indicator.id}')" title="${indicator.visible !== false ? 'Hide' : 'Show'}">
${indicator.visible !== false ? '👁' : '👁‍🗨'}
</button>
<span class="indicator-name">${label}</span>
${showPresets}
<div class="indicator-info">
<span class="indicator-name">${label}</span>
<span class="indicator-desc">${description}</span>
</div>
${showPresets || ''}
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite && window.toggleFavorite('${indicator.type}')" title="Add to favorites">
${isFavorite ? '★' : '☆'}
</button>
@ -557,6 +635,7 @@ window.updateIndicatorColor = function(id, index, color) {
if (!indicator) return;
indicator.params[`_color_${index}`] = color;
saveActiveIndicators();
drawIndicatorsOnChart();
};
@ -568,6 +647,7 @@ window.updateIndicatorSetting = function(id, key, value) {
indicator.lastSignalTimestamp = null;
indicator.lastSignalType = null;
indicator.cachedResults = null; // Clear cache when params change
saveActiveIndicators();
drawIndicatorsOnChart();
};
@ -579,6 +659,7 @@ window.clearAllIndicators = function() {
});
activeIndicators = [];
configuringId = null;
saveActiveIndicators();
renderIndicatorPanel();
drawIndicatorsOnChart();
}
@ -590,6 +671,7 @@ window.toggleAllIndicatorsVisibility = function() {
ind.visible = !allVisible;
});
saveActiveIndicators();
drawIndicatorsOnChart();
renderIndicatorPanel();
}
@ -608,6 +690,7 @@ function removeIndicatorById(id) {
configuringId = null;
}
saveActiveIndicators();
renderIndicatorPanel();
drawIndicatorsOnChart();
}
@ -773,18 +856,25 @@ function addIndicator(type) {
// Override with Hurst-specific defaults
if (type === 'hurst') {
const hurstCount = activeIndicators.filter(ind => ind.type === 'hurst').length;
const color = hurstCount > 0 ? '#ff9800' : '#9e9e9e';
params._lineWidth = 1;
params.timeframe = 'chart';
params.markerBuyShape = 'custom';
params.markerSellShape = 'custom';
params.markerBuyColor = '#9e9e9e';
params.markerSellColor = '#9e9e9e';
params.markerBuyColor = color;
params.markerSellColor = color;
params.markerBuyCustom = '▲';
params.markerSellCustom = '▼';
}
metadata.plots.forEach((plot, idx) => {
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
if (type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst').length > 0) {
params[`_color_${idx}`] = '#ff9800';
} else {
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
}
});
metadata.inputs.forEach(input => {
params[input.name] = input.default;
@ -801,7 +891,7 @@ function addIndicator(type) {
paneHeight: 120 // default 120px
});
// Don't set configuringId so indicators are NOT expanded by default
saveActiveIndicators();
renderIndicatorPanel();
drawIndicatorsOnChart();
};
@ -810,34 +900,154 @@ function saveUserPresets() {
localStorage.setItem('indicator_presets', JSON.stringify(userPresets));
}
// Custom Primitive for filling area between two lines
class SeriesAreaFillPrimitive {
constructor(data, color) {
this._data = data || [];
this._color = color || 'rgba(128, 128, 128, 0.05)';
this._paneViews = [new SeriesAreaFillPaneView(this)];
}
setData(data) {
this._data = data;
this._requestUpdate?.();
}
setColor(color) {
this._color = color;
this._requestUpdate?.();
}
attached(param) {
this._chart = param.chart;
this._series = param.series;
this._requestUpdate = param.requestUpdate;
this._requestUpdate();
}
detached() {
this._chart = undefined;
this._series = undefined;
this._requestUpdate = undefined;
}
updateAllViews() {
this._requestUpdate?.();
}
paneViews() {
return this._paneViews;
}
}
class SeriesAreaFillPaneView {
constructor(source) {
this._source = source;
}
renderer() {
return new SeriesAreaFillRenderer(this._source);
}
}
class SeriesAreaFillRenderer {
constructor(source) {
this._source = source;
}
draw(target) {
if (!this._source._chart || !this._source._series || this._source._data.length === 0) return;
target.useBitmapCoordinateSpace((scope) => {
const ctx = scope.context;
const series = this._source._series;
const chart = this._source._chart;
const data = this._source._data;
const color = this._source._color;
const ratio = scope.horizontalPixelRatio;
// Optimization: Get visible range to avoid iterating over thousands of historical points
const timeScale = chart.timeScale();
const visibleRange = timeScale.getVisibleLogicalRange();
if (!visibleRange) return;
ctx.save();
ctx.beginPath();
let started = false;
// Find start and end indices in data based on visible range for massive performance boost
// Since data is sorted by time, we could use binary search, but even a linear scan
// with visibility check is better than drawing everything.
// Draw top line (upper) forward
for (let i = 0; i < data.length; i++) {
const point = data[i];
const timeCoordinate = timeScale.timeToCoordinate(point.time);
// Skip points far outside the visible area
if (timeCoordinate === null) {
if (started) break; // We've passed the visible range
continue;
}
const upperY = series.priceToCoordinate(point.upper);
if (upperY === null) continue;
const x = timeCoordinate * ratio;
const y = upperY * ratio;
if (!started) {
ctx.moveTo(x, y);
started = true;
} else {
ctx.lineTo(x, y);
}
}
// Draw bottom line (lower) backward
for (let i = data.length - 1; i >= 0; i--) {
const point = data[i];
const timeCoordinate = timeScale.timeToCoordinate(point.time);
if (timeCoordinate === null) {
if (started && i < data.length / 2) break;
continue;
}
const lowerY = series.priceToCoordinate(point.lower);
if (lowerY === null) continue;
const x = timeCoordinate * ratio;
const y = lowerY * ratio;
ctx.lineTo(x, y);
}
if (started) {
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
ctx.restore();
});
}
}
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
// Recalculate with current TF candles (or use cached if they exist and are the correct length)
let results = indicator.cachedResults;
if (!results || !Array.isArray(results) || results.length !== candles.length) {
console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`);
// console.log(`[renderIndicatorOnPane] ${indicator.name}: Recalculating... (${candles.length} candles)`);
results = instance.calculate(candles);
indicator.cachedResults = results;
}
if (!results || !Array.isArray(results)) {
console.error(`[renderIndicatorOnPane] ${indicator.name}: Failed to get valid results (got ${typeof results})`);
return;
}
if (results.length !== candles.length) {
console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`);
}
// Clear previous series for this indicator
if (indicator.series && indicator.series.length > 0) {
indicator.series.forEach(s => {
try {
window.dashboard.chart.removeSeries(s);
} catch(e) {}
});
}
indicator.series = [];
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
const lineWidth = indicator.params._lineWidth || 1;
@ -845,18 +1055,19 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null;
let isObjectResult = firstNonNull && typeof firstNonNull === 'object' && !Array.isArray(firstNonNull);
// Fallback: If results are all null (e.g. during warmup or MTF fetch),
// use metadata to determine if it SHOULD be an object result
if (!firstNonNull && meta.plots && meta.plots.length > 1) {
isObjectResult = true;
}
// Also check if the only plot has a specific ID that isn't just a number
if (!firstNonNull && meta.plots && meta.plots.length === 1 && meta.plots[0].id !== 'value') {
isObjectResult = true;
}
let plotsCreated = 0;
let dataPointsAdded = 0;
indicator.series = indicator.series || [];
let seriesIdx = 0;
// Special logic for Hurst fill
let hurstFillData = [];
const isFirstHurst = indicator.type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst')[0]?.id === indicator.id;
meta.plots.forEach((plot, plotIdx) => {
if (isObjectResult) {
@ -865,116 +1076,108 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
}
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
const data = [];
let firstDataIndex = -1;
for (let i = 0; i < candles.length; i++) {
let value;
if (isObjectResult) {
value = results[i]?.[plot.id];
if (isFirstHurst && results[i]) {
if (!hurstFillData[i]) hurstFillData[i] = { time: candles[i].time };
if (plot.id === 'upper') hurstFillData[i].upper = value;
if (plot.id === 'lower') hurstFillData[i].lower = value;
}
} else {
value = results[i];
}
if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) {
if (firstDataIndex === -1) {
firstDataIndex = i;
}
data.push({
time: candles[i].time,
value: value
});
data.push({ time: candles[i].time, value: value });
}
}
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: ${data.length} data points created, first data at index ${firstDataIndex}/${candles.length}`);
if (data.length === 0) return;
if (data.length === 0) {
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: No data to render`);
return;
}
console.log(`[renderIndicatorOnPane] ${indicator.name} plot ${plotIdx}: Creating series with ${data.length} data points [${data[0].time} to ${data[data.length - 1].time}]`);
let series;
let series = indicator.series[seriesIdx];
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 || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle,
title: plot.title || '',
priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false
}, paneIndex);
const seriesOptions = {
color: plotColor,
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle,
title: '',
priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false,
priceFormat: { type: 'price', precision: 0, minMove: 1 }
};
if (!series) {
// Create new series if it doesn't exist
if (plot.type === 'histogram') {
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, seriesOptions, paneIndex);
} else if (plot.type === 'baseline') {
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
...seriesOptions,
baseValue: { type: 'price', price: plot.baseValue || 0 },
topLineColor: plot.topLineColor || plotColor,
topFillColor1: plot.topFillColor1 || plotColor,
topFillColor2: '#00000000',
bottomFillColor1: '#00000000',
bottomColor: plot.bottomColor || '#00000000',
}, paneIndex);
} else {
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, seriesOptions, paneIndex);
}
indicator.series[seriesIdx] = series;
} else {
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: plotColor,
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle,
title: '',
priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false
}, paneIndex);
// Update existing series options
series.applyOptions(seriesOptions);
}
series.setData(data);
indicator.series.push(series);
plotsCreated++;
console.log(`Created series for ${indicator.id}, plot=${plot.id}, total series now=${indicator.series.length}`);
seriesIdx++;
// Create horizontal band lines for RSI
// RSI bands
if (meta.name === 'RSI' && indicator.series.length > 0) {
const mainSeries = indicator.series[0];
const overbought = indicator.params.overbought || 70;
const oversold = indicator.params.oversold || 30;
// Remove existing price lines first
while (indicator.bands && indicator.bands.length > 0) {
try {
indicator.bands.pop();
} catch(e) {}
try { indicator.bands.pop(); } catch(e) {}
}
indicator.bands = indicator.bands || [];
// Create overbought band line
indicator.bands.push(mainSeries.createPriceLine({
price: overbought,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
price: overbought, color: '#787B86', lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: ''
}));
// Create oversold band line
indicator.bands.push(mainSeries.createPriceLine({
price: oversold,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
price: oversold, color: '#787B86', lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: ''
}));
}
});
// Cleanup extra series if any
while (indicator.series.length > seriesIdx) {
const extra = indicator.series.pop();
try { window.dashboard.chart.removeSeries(extra); } catch(e) {}
}
// Attach Hurst Fill Primitive
if (isFirstHurst && hurstFillData.length > 0 && indicator.series.length > 0) {
const validFillData = hurstFillData.filter(d => d && d.time && d.upper !== undefined && d.lower !== undefined);
if (!indicator.fillPrimitive) {
indicator.fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)');
indicator.series[0].attachPrimitive(indicator.fillPrimitive);
} else {
indicator.fillPrimitive.setData(validFillData);
}
}
}
// Completely redraw indicators (works for both overlay and pane)
@ -1034,13 +1237,13 @@ export function drawIndicatorsOnChart() {
const activeIndicators = getActiveIndicators();
// Remove all existing series
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
});
ind.series = [];
});
// Remove all existing series - OPTIMIZATION: Removed aggressive clearing to allow reuse
// activeIndicators.forEach(ind => {
// ind.series?.forEach(s => {
// try { window.dashboard.chart.removeSeries(s); } catch(e) {}
// });
// ind.series = [];
// });
const lineStyleMap = {
'solid': LightweightCharts.LineStyle.Solid,
@ -1059,6 +1262,13 @@ export function drawIndicatorsOnChart() {
// Process all indicators, filtering by visibility
activeIndicators.forEach(ind => {
if (ind.visible === false || ind.visible === 'false') {
// Hide existing series if they exist by setting empty data
if (ind.series && ind.series.length > 0) {
ind.series.forEach(s => s.setData([]));
}
if (ind.fillPrimitive) {
ind.fillPrimitive.setData([]);
}
return;
}
@ -1157,6 +1367,7 @@ function resetIndicator(id) {
indicator.params[input.name] = input.default;
});
saveActiveIndicators();
renderIndicatorPanel();
drawIndicatorsOnChart();
}
@ -1173,6 +1384,7 @@ function toggleIndicatorVisibility(id) {
indicator.visible = indicator.visible === false;
saveActiveIndicators();
// Full redraw to ensure all indicators render correctly
if (typeof drawIndicatorsOnChart === 'function') {
drawIndicatorsOnChart();

View File

@ -550,8 +550,8 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
color: plotColor,
priceFormat: {
type: 'price',
precision: 4,
minMove: 0.0001
precision: 0,
minMove: 1
},
priceLineVisible: false,
lastValueVisible: false

View File

@ -0,0 +1,349 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>Crypto Dashboard - BTC/USD</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<style data-purpose="base-styles">
body {
background-color: #0d1421;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
}
.text-muted {
color: #8fa2b3;
}
.bg-dark-surface {
background-color: #0d1421;
}
.border-dark {
border-color: #1e293b;
}
.indicator-modal {
background-color: #1a2333;
border: 1px solid #2d3a4f;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
box-shadow: 0 -4px 30px rgba(0, 0, 0, 0.6);
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.text-bullish {
color: #26d367;
}
.text-bearish {
color: #ff4d4d;
}
.bg-accent {
background-color: #2d3a4f;
}
</style>
<style data-purpose="chart-customization">
.grid-line {
stroke: #1e293b;
stroke-width: 0.5;
}
.candle-bull {
fill: #26d367;
stroke: #26d367;
}
.candle-bear {
fill: #ff4d4d;
stroke: #ff4d4d;
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
</head>
<body class="flex flex-col h-screen overflow-hidden">
<!-- BEGIN: Top Navigation Bar -->
<header class="p-4 flex items-center justify-between bg-dark-surface border-b border-dark flex-shrink-0">
<div class="flex items-center space-x-3 bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
<svg class="w-4 h-4 text-muted" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></svg>
<span class="font-bold text-sm">BTC/USD</span>
<svg class="w-3 h-3 text-muted" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path d="M9 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></svg>
</div>
<div class="flex space-x-1">
<button class="px-2 py-1 text-xs text-muted hover:bg-[#2d3a4f] rounded">1m</button>
<button class="px-2 py-1 text-xs text-muted hover:bg-[#2d3a4f] rounded">5m</button>
<button class="px-2 py-1 text-xs text-muted hover:bg-[#2d3a4f] rounded">15m</button>
<button class="px-2 py-1 text-xs text-muted hover:bg-[#2d3a4f] rounded">1h</button>
<button class="px-2 py-1 text-xs text-muted hover:bg-[#2d3a4f] rounded">4h</button>
<button class="px-2 py-1 text-xs bg-[#2d3a4f] text-white rounded font-bold">D</button>
</div>
</header>
<!-- END: Top Navigation Bar -->
<!-- BEGIN: Main Content -->
<main class="flex-1 flex flex-col overflow-hidden relative">
<!-- BEGIN: Price Statistics -->
<section class="grid grid-cols-4 gap-2 px-4 py-4 border-b border-dark flex-shrink-0 bg-dark-surface" id="priceHeader">
<div>
<p class="text-[10px] text-muted uppercase">Price</p>
<p class="text-lg font-bold">70339.00</p>
</div>
<div>
<p class="text-[10px] text-muted uppercase">Change <span class="text-bearish"></span></p>
<p class="text-lg font-bold text-bearish">-4.84%</p>
</div>
<div>
<p class="text-[10px] text-muted uppercase">High ↗</p>
<p class="text-lg font-bold">74250.00</p>
</div>
<div>
<p class="text-[10px] text-muted uppercase">Low ↘</p>
<p class="text-lg font-bold">70279.00</p>
</div>
</section>
<!-- END: Price Statistics -->
<div class="flex-1 relative overflow-hidden">
<!-- BEGIN: Candlestick Chart -->
<section class="absolute inset-0 bg-[#0d1421]" data-purpose="chart-container">
<canvas class="w-full h-full" id="tradingChart"></canvas>
<!-- Price Axis Labels -->
<div class="absolute right-0 top-0 bottom-0 w-16 flex flex-col justify-between text-[10px] text-muted py-4 pointer-events-none">
<span>77500.00</span>
<span>75000.00</span>
<span>72500.00</span>
<div class="bg-bearish text-white px-1 py-0.5 rounded-l text-[10px] relative z-10" id="priceLabel">70339.00</div>
<span>67500.00</span>
<span>65000.00</span>
<span>62500.00</span>
</div>
<!-- Time Axis Labels -->
<div class="absolute bottom-2 left-4 right-16 flex justify-between text-[10px] text-muted pointer-events-none">
<span>01/03 01:00</span>
<span>04/03 01:00</span>
<span>09/03 01:00</span>
<span>14/03 01:00</span>
<span>17/03 01:00</span>
</div>
</section>
<!-- END: Candlestick Chart -->
<!-- BEGIN: Indicators Modal -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[95%] h-[calc(100%-16px)] indicator-modal z-40 overflow-hidden flex flex-col mt-2" id="indicatorsModal">
<!-- Modal Header -->
<div class="flex items-center justify-between p-4 border-b border-[#2d3a4f]">
<span class="material-symbols-outlined text-muted cursor-pointer">close</span>
<h3 class="text-md font-bold">Indicators</h3>
<button class="bg-[#2d3a4f] px-3 py-1 rounded text-xs font-bold border border-[#3d4b63]">Strategy</button>
</div>
<!-- Search Bar -->
<div class="px-4 py-3">
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-3 text-muted text-lg">search</span>
<input class="w-full bg-[#0d1421] border border-[#2d3a4f] rounded-md py-2 pl-10 pr-4 text-sm focus:ring-1 focus:ring-blue-500" placeholder="Search indicators..." type="text"/>
</div>
</div>
<!-- Filters -->
<div class="flex space-x-2 px-4 pb-4 overflow-x-auto no-scrollbar">
<button class="bg-white text-black px-3 py-1 rounded-full text-xs font-bold whitespace-nowrap">All</button>
<button class="bg-[#2d3a4f] text-white px-3 py-1 rounded-full text-xs font-bold whitespace-nowrap">Trend</button>
<button class="bg-[#2d3a4f] text-white px-3 py-1 rounded-full text-xs font-bold whitespace-nowrap">Momentum</button>
<button class="bg-[#2d3a4f] text-white px-3 py-1 rounded-full text-xs font-bold whitespace-nowrap">Volatility</button>
</div>
<!-- List -->
<div class="flex-1 overflow-y-auto">
<div class="px-4 py-2 text-[10px] font-bold text-muted uppercase tracking-wider">Available Indicators</div>
<div class="px-4 py-3 border-b border-[#2d3a4f] hover:bg-[#252f3f] flex items-center justify-between">
<div>
<p class="text-sm font-medium">Moving Average</p>
<p class="text-[10px] text-muted">(SMA/EMA/RMA/WMA/VWMA)</p>
</div>
<div class="flex space-x-3 text-muted">
<span class="material-symbols-outlined text-xl">add</span>
<span class="material-symbols-outlined text-xl">star</span>
</div>
</div>
<div class="px-4 py-3 border-b border-[#2d3a4f] hover:bg-[#252f3f] flex items-center justify-between">
<div>
<p class="text-sm font-medium">Moving Average Convergence Divergence</p>
<p class="text-[10px] text-muted">MACD - trend &amp; momentum</p>
</div>
<div class="flex space-x-3 text-muted">
<span class="material-symbols-outlined text-xl">add</span>
<span class="material-symbols-outlined text-xl">star</span>
</div>
</div>
<div class="px-4 py-3 border-b border-[#2d3a4f] hover:bg-[#252f3f] flex items-center justify-between">
<div>
<p class="text-sm font-medium">RSI</p>
<p class="text-[10px] text-muted">Relative Strength Index</p>
</div>
<div class="flex space-x-3 text-muted">
<span class="material-symbols-outlined text-xl">add</span>
<span class="material-symbols-outlined text-xl">star</span>
</div>
</div>
<div class="px-4 py-3 border-b border-[#2d3a4f] hover:bg-[#252f3f] flex items-center justify-between">
<div>
<p class="text-sm font-medium">Bollinger Bands</p>
<p class="text-[10px] text-muted">Volatility bands around a moving average</p>
</div>
<div class="flex space-x-3 text-muted">
<span class="material-symbols-outlined text-xl">add</span>
<span class="material-symbols-outlined text-xl">star</span>
</div>
</div>
<div class="px-4 py-3 border-b border-[#2d3a4f] hover:bg-[#252f3f] flex items-center justify-between">
<div>
<p class="text-sm font-medium">Stochastic Oscillator</p>
<p class="text-[10px] text-muted">Compares close to high-low range</p>
</div>
<div class="flex space-x-3 text-muted">
<span class="material-symbols-outlined text-xl">add</span>
<span class="material-symbols-outlined text-xl">star</span>
</div>
</div>
</div>
</div>
<!-- END: Indicators Modal -->
</div>
</main>
<!-- END: Main Content -->
<!-- BEGIN: Bottom Tab Navigation -->
<nav class="fixed bottom-0 w-full bg-[#0d1421] border-t border-[#1e293b] flex justify-around items-center py-2 px-1 z-50">
<div class="flex flex-col items-center text-muted">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></svg>
<span class="text-[10px] mt-1">Markets</span>
</div>
<div class="flex flex-col items-center text-muted">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path d="M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z"></path></svg>
<span class="text-[10px] mt-1">Chart</span>
</div>
<div class="flex flex-col items-center text-white">
<span class="material-symbols-outlined w-6 h-6 flex items-center justify-center">query_stats</span>
<span class="text-[10px] mt-1 font-bold">Indicators</span>
</div>
<div class="flex flex-col items-center text-muted">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></svg>
<span class="text-[10px] mt-1">Analysis</span>
</div>
<div class="flex flex-col items-center text-muted">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path></svg>
<span class="text-[10px] mt-1">More</span>
</div>
</nav>
<!-- END: Bottom Tab Navigation -->
<!-- BEGIN: Chart Logic -->
<script data-purpose="chart-rendering">
const canvas = document.getElementById('tradingChart');
const ctx = canvas.getContext('2d');
function drawChart() {
const container = canvas.parentNode;
const rect = container.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
const width = rect.width;
const height = rect.height;
const gridColor = '#1e293b';
const bullColor = '#26d367';
const bearColor = '#ff4d4d';
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = gridColor;
ctx.lineWidth = 0.5;
for(let i = 1; i < 8; i++) {
const y = (height / 8) * i;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
for(let i = 1; i < 6; i++) {
const x = (width / 6) * i;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
const candleWidth = 8;
const gap = 4;
const startX = 20;
const data = [
[65000, 66000, 64500, 65500],
[65500, 67000, 65000, 66500],
[66500, 66800, 64000, 64500],
[64500, 65000, 63000, 63500],
[63500, 66000, 63000, 65800],
[65800, 67500, 65500, 67000],
[67000, 69000, 66500, 68500],
[68500, 72000, 68000, 71500],
[71500, 73000, 70000, 72500],
[72500, 72500, 70000, 71000],
[71000, 73500, 70500, 73000],
[73000, 74000, 72000, 73500],
[73500, 76500, 73000, 76000],
[76000, 76500, 69000, 70000],
[70000, 71000, 68000, 68500],
[68500, 70000, 67500, 69500],
[69500, 71000, 66000, 67000],
[67000, 68000, 65000, 66000],
[66000, 68500, 65500, 68000],
[68000, 71500, 67500, 71000],
[71000, 72000, 69000, 69500],
[69500, 70500, 68500, 69000],
[69000, 70000, 68000, 69800],
[69800, 71500, 69500, 71200],
[71200, 73000, 71000, 72500],
[72500, 73500, 72000, 73200],
[73200, 74500, 72500, 73800],
[73800, 73800, 70000, 70339]
];
const minPrice = 61000;
const maxPrice = 78000;
const priceToY = (price) => height - ((price - minPrice) / (maxPrice - minPrice)) * height;
data.forEach((d, i) => {
const x = startX + i * (candleWidth + gap);
const openY = priceToY(d[0]);
const highY = priceToY(d[1]);
const lowY = priceToY(d[2]);
const closeY = priceToY(d[3]);
const isBull = d[3] >= d[0];
const color = isBull ? bullColor : bearColor;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x + candleWidth / 2, highY);
ctx.lineTo(x + candleWidth / 2, lowY);
ctx.stroke();
const bodyHeight = Math.abs(openY - closeY);
const bodyY = Math.min(openY, closeY);
ctx.fillRect(x, bodyY, candleWidth, Math.max(bodyHeight, 1));
});
const currentPriceY = priceToY(70339);
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(255, 77, 77, 0.7)';
ctx.beginPath();
ctx.moveTo(0, currentPriceY);
ctx.lineTo(width, currentPriceY);
ctx.stroke();
ctx.setLineDash([]);
}
window.addEventListener('load', drawChart);
window.addEventListener('resize', drawChart);
</script>
<!-- END: Chart Logic -->
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

@ -0,0 +1,301 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>Crypto Dashboard - BTC/USD</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<style data-purpose="base-styles">
body {
background-color: #0d1421; /* Match background color from SCREEN_6 indicator panel */
color: #ffffff;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
}
.text-muted {
color: #8fa2b3; /* Consistent with SCREEN_6 theme */
}
.bg-dark-surface {
background-color: #0d1421;
}
.border-dark {
border-color: #1e293b; /* Deep slate divider color */
}
.bg-card-ai {
background-color: #161e2e; /* AI Analysis theme card background from IMAGE_8 */
border: 1px solid #2d3a4f;
}
</style>
<style data-purpose="chart-customization">
/* Custom grid lines for the chart */
.grid-line {
stroke: #1e293b;
stroke-width: 1;
}
/* Orange color for all candles as requested */
.candle-orange {
fill: #f0b90b;
stroke: #f0b90b;
}
.wick-orange {
stroke: #f0b90b;
stroke-width: 1.5;
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
</head>
<body class="flex flex-col h-screen overflow-hidden">
<!-- BEGIN: Top Navigation Bar (Using COMPONENTS_17 TopAppBar logic) -->
<header class="bg-[#0f131e] fixed top-0 w-full z-50 h-16 px-6 flex items-center justify-between border-b border-[#1b1f2b]">
<div class="flex items-center space-x-3 bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span>
<span class="font-bold text-sm text-[#dfe2f2]">BTC/USD</span>
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">chevron_right</span>
</div>
<div class="flex space-x-1 items-center">
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">1m</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">5m</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">15m</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">1h</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">4h</button>
<button class="px-2 py-1 text-xs bg-[#2962ff] text-white rounded font-bold">D</button>
</div>
</header>
<!-- BEGIN: Main Content -->
<main class="flex-1 overflow-y-auto pt-16 pb-20">
<!-- BEGIN: Price Statistics -->
<section class="grid grid-cols-4 gap-2 px-4 py-4 border-b border-[#1e293b] bg-[#0d1421]">
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Price</p>
<p class="text-lg font-bold">70339</p>
</div>
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Change <span class="text-red-500"></span></p>
<p class="text-lg font-bold text-red-500">-4.84%</p>
</div>
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">High ↗</p>
<p class="text-lg font-bold">74250</p>
</div>
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Low ↘</p>
<p class="text-lg font-bold">70279</p>
</div>
</section>
<!-- END: Price Statistics -->
<!-- BEGIN: Candlestick Chart -->
<section class="relative w-full bg-[#0d1421] h-[65vh]" data-purpose="chart-container">
<canvas class="w-full h-full" id="tradingChart"></canvas>
<!-- Price Axis Labels -->
<div class="absolute right-0 top-0 bottom-0 w-16 flex flex-col justify-between text-[10px] text-[#8fa2b3] py-4 pointer-events-none">
<span>77500</span>
<span>75000</span>
<span>72500</span>
<div class="bg-red-500 text-white px-1 py-0.5 rounded-l text-[10px]">70339</div>
<span>67500</span>
<span>65000</span>
<span>62500</span>
</div>
<!-- Time Axis Labels -->
<div class="absolute bottom-2 left-4 right-16 flex justify-between text-[10px] text-[#8fa2b3] pointer-events-none">
<span>01/03 01:00</span>
<span>04/03 01:00</span>
<span>09/03 01:00</span>
<span>14/03 01:00</span>
<span>17/03 01:00</span>
</div>
</section>
<!-- END: Candlestick Chart -->
<!-- BEGIN: Technical Analysis Section -->
<section class="px-4 py-6 bg-[#0d1421]">
<h2 class="text-lg font-bold mb-4 flex items-center gap-2">
<span class="material-symbols-outlined text-[#b6c4ff]">analytics</span>
Technical Analysis
</h2>
<div class="grid grid-cols-2 gap-4">
<!-- Best Moving Averages Card -->
<div class="p-4 rounded-xl bg-card-ai" data-purpose="ta-card">
<p class="text-[10px] font-bold text-[#8fa2b3] uppercase mb-4 tracking-wider">Best Moving Averages</p>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">MA 44</span>
<div class="text-right">
<p class="font-bold text-sm text-[#dfe2f2]">68817.32</p>
<p class="text-[10px] text-green-500">+2.3%</p>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">MA 125</span>
<div class="text-right">
<p class="font-bold text-sm text-[#dfe2f2]">82087.39</p>
<p class="text-[10px] text-red-500">-14.3%</p>
</div>
</div>
</div>
</div>
<!-- Support / Resistance Card -->
<div class="p-4 rounded-xl bg-card-ai" data-purpose="ta-card">
<p class="text-[10px] font-bold text-[#8fa2b3] uppercase mb-4 tracking-wider">Support / Resistance</p>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">Resistance</span>
<span class="font-bold text-sm text-right text-[#dfe2f2]">75972</span>
</div>
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">Support</span>
<span class="font-bold text-sm text-right text-[#dfe2f2]">62983</span>
</div>
</div>
</div>
</div>
</section>
<!-- END: Technical Analysis Section -->
</main>
<!-- END: Main Content -->
<!-- BEGIN: Bottom Tab Navigation (Using COMPONENTS_17 BottomNavBar logic) -->
<nav class="fixed bottom-0 w-full bg-[#0f131e] border-t border-[#434656]/15 flex justify-around items-center h-16 z-50 px-2 shadow-[0px_-4px_12px_rgba(0,0,0,0.3)]">
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">show_chart</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Markets</span>
</div>
<div class="flex flex-col items-center justify-center text-[#2962ff] dark:text-[#b6c4ff] bg-[#2962ff]/10 rounded-xl px-3 py-1 transition-transform duration-200">
<span class="material-symbols-outlined">candlestick_chart</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Chart</span>
</div>
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">query_stats</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Indicators</span>
</div>
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">analytics</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Analysis</span>
</div>
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">more_horiz</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">More</span>
</div>
</nav>
<!-- BEGIN: Chart Logic -->
<script data-purpose="chart-rendering"> const canvas = document.getElementById('tradingChart');
const ctx = canvas.getContext('2d');
function drawChart() {
const container = canvas.parentNode;
const rect = container.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
const width = rect.width;
const height = rect.height;
const orange = '#f0b90b';
const gridColor = '#1e293b';
// Axis width offset (matches the w-16 div on the right)
const axisWidth = 64;
const drawingWidth = width - axisWidth;
ctx.clearRect(0, 0, width, height);
// Draw Grid
ctx.strokeStyle = gridColor;
ctx.lineWidth = 0.5;
for(let i = 1; i < 8; i++) {
const y = (height / 8) * i;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(drawingWidth, y);
ctx.stroke();
}
for(let i = 1; i < 6; i++) {
const x = (drawingWidth / 6) * i;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
// Simulated data points [open, high, low, close]
const data = [
[65000, 66000, 64500, 65500],
[65500, 67000, 65000, 66500],
[66500, 66800, 64000, 64500],
[64500, 65000, 63000, 63500],
[63500, 66000, 63000, 65800],
[65800, 67500, 65500, 67000],
[67000, 69000, 66500, 68500],
[68500, 72000, 68000, 71500],
[71500, 73000, 70000, 72500],
[72500, 72500, 70000, 71000],
[71000, 73500, 70500, 73000],
[73000, 74000, 72000, 73500],
[73500, 76500, 73000, 76000],
[76000, 76500, 69000, 70000],
[70000, 71000, 68000, 68500],
[68500, 70000, 67500, 69500],
[69500, 71000, 66000, 67000],
[67000, 68000, 65000, 66000],
[66000, 68500, 65500, 68000],
[68000, 71500, 67500, 71000],
[71000, 72000, 69000, 69500],
[69500, 70500, 68500, 69000],
[69000, 70000, 68000, 69800],
[69800, 71500, 69500, 71200],
[71200, 73000, 71000, 72500],
[72500, 73500, 72000, 73200],
[73200, 74500, 72500, 73800],
[73800, 73800, 70000, 70339]
];
const minPrice = 61000;
const maxPrice = 78000;
const priceToY = (price) => height - ((price - minPrice) / (maxPrice - minPrice)) * height;
const candleWidth = 8;
const gap = 4;
// Adjust startX to move candles further left and avoid overlap
const startX = 10;
data.forEach((d, i) => {
const x = startX + i * (candleWidth + gap);
// Check to ensure we don't draw over the right axis labels
if (x + candleWidth > drawingWidth) return;
const openY = priceToY(d[0]);
const highY = priceToY(d[1]);
const lowY = priceToY(d[2]);
const closeY = priceToY(d[3]);
ctx.strokeStyle = orange;
ctx.fillStyle = orange;
ctx.lineWidth = 1;
// Wick
ctx.beginPath();
ctx.moveTo(x + candleWidth / 2, highY);
ctx.lineTo(x + candleWidth / 2, lowY);
ctx.stroke();
// Body
const bodyHeight = Math.abs(openY - closeY);
const bodyY = Math.min(openY, closeY);
ctx.fillRect(x, bodyY, candleWidth, Math.max(bodyHeight, 1));
});
// Horizontal current price line
const currentPriceY = priceToY(70339);
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(239, 68, 68, 0.7)';
ctx.beginPath();
ctx.moveTo(0, currentPriceY);
ctx.lineTo(drawingWidth, currentPriceY);
ctx.stroke();
ctx.setLineDash([]);
}
window.addEventListener('load', drawChart);
window.addEventListener('resize', drawChart);</script>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@ -0,0 +1,108 @@
## Project Brief: TradingView-Inspired Mobile App Redesign for Enhanced Crypto Trading Experience
**Project Name:** Project Phoenix: TradingView-Style Crypto App UI/UX Overhaul
**Version:** 1.0
**Date:** October 26, 2023
**Author:** Product Manager
---
### 1. Introduction
This document outlines the requirements for a significant mobile app redesign, aiming to elevate the user experience for cryptocurrency traders. The primary goal is to transform the app's aesthetic and functionality to align with the professional, data-rich, and intuitive design standards set by industry leaders like TradingView. This initial phase focuses on core charting capabilities, technical analysis integration, and innovative AI-driven insights.
### 2. Problem Statement / Background
The current mobile application's design and functionality do not meet the sophisticated expectations of modern cryptocurrency traders. It lacks the professional-grade charting, comprehensive technical analysis tools, and visually appealing interface found in leading platforms, leading to potential user dissatisfaction and limited analytical capabilities within the app.
### 3. Project Goals / Objectives
* **Elevate UI/UX:** Overhaul the mobile application's user interface and user experience to reflect the professional, data-centric, and intuitive design principles of TradingView.
* **Enhance Charting:** Implement a high-fidelity, interactive charting experience with advanced customization options.
* **Integrate Technical Analysis:** Provide accessible and robust tools for technical analysis, including indicators and key levels.
* **Introduce AI-Driven Insights:** Integrate AI-powered analysis to provide actionable trading signals and market sentiment.
* **Improve Engagement:** Increase user engagement and satisfaction by offering a superior and more complete trading analysis environment on mobile.
### 4. Target Audience
* Experienced and novice cryptocurrency traders.
* Investors and analysts who require professional-grade market analysis tools.
* Users seeking a sophisticated, data-rich, and visually appealing mobile trading experience.
### 5. Key Features & Screens
This phase of the redesign will focus on the following key screens and functionalities:
#### 5.1 TradingView Style Crypto Dashboard (Core Charting Screen)
* **Description:** A professional cryptocurrency trading dashboard heavily inspired by TradingView, serving as the central hub for market analysis.
* **Components:**
* **Search Bar:** Prominently placed for quick search of trading pairs (e.g., BTC/USD).
* **Time Interval Selector:** Horizontal selector for popular timeframes (1m, 5m, 15m, 1h, 4h, D).
* **High-Quality Candlestick Chart:** Interactive chart with customizable themes (initial: orange/black; potential: green/red), grid lines, and smooth performance.
* **Price Data Display:** Clear display of current price, change percentage, high, and low values for the selected pair.
* **Technical Analysis Section:** Integrated cards below the chart for key insights, such as "Best Moving Averages" (MA 44, MA 125) and "Support/Resistance" levels.
* **Theming:** Deep charcoal/navy dark mode theme with crisp typography and subtle borders.
* **Mobile Navigation:** Clear bottom navigation bar including "Market", "Chart", "Trade", "Alerts", "Menu".
#### 5.2 Indicators Selection Modal
* **Description:** A clean and searchable overlay for users to discover and add technical indicators to their charts, mirroring TradingView's indicator library.
* **Components:**
* **Search Bar:** "Search indicators..." functionality at the top.
* **Category Chips:** Filter indicators by type (All, Trend, Momentum, Volatility).
* **Available Indicators List:** Scrollable list of indicators (e.g., Moving Average, MACD, RSI, Bollinger Bands).
* **Actions:** '+' icon to add an indicator to the chart, 'star' icon to favorite indicators for quick access.
* **Theming:** Semi-transparent dark overlay over the main chart, maintaining the refined dark theme and consistent UI elements.
#### 5.3 AI Analysis Insights
* **Description:** A dedicated detailed view providing AI-driven analysis and insights for a specific trading pair, translating complex data into actionable information.
* **Components:**
* **Summary Header:** Clean display of the pair name and current price.
* **AI Insight Card:** Prominent card summarizing the technical sentiment (e.g., 'Strong Buy', 'Neutral', 'Strong Sell').
* **Market Sentiment Gauge:** Interactive visual representation of overall market sentiment.
* **Key Signals List:** Highlights identified bullish and bearish factors from the AI analysis.
* **Theming:** Maintains the professional, data-heavy but organized dark mode aesthetic with neon accent colors for signals to enhance readability.
### 6. High-Level User Stories
* As a trader, I want to view a professional, high-quality candlestick chart for any crypto pair so I can perform in-depth technical analysis.
* As a trader, I want to quickly switch between different time intervals on the chart so I can analyze price action at various granularities.
* As a trader, I want to easily find and add technical indicators to my chart from a categorized and searchable library so I can customize my analysis.
* As a trader, I want to see a concise, AI-generated summary of market sentiment and key signals for a trading pair so I can quickly gauge its potential.
* As a trader, I want a consistent dark-themed interface that is easy on the eyes and professional-looking so I can focus on my trading analysis.
### 7. Technical Considerations
* **Charting Library:** Integration with a robust and performant charting library capable of rendering complex candlestick charts and indicators (e.g., Lightweight Charts, TradingView Charting Library, or a custom solution).
* **Real-time Data:** Secure and efficient real-time data streaming for price updates, volume, and indicator calculations.
* **Backend Integration:** APIs to fetch market data, integrate AI analysis results, and manage user preferences (e.g., favorited indicators).
* **Performance:** Optimization for smooth scrolling, zooming, and rapid data updates on mobile devices.
* **Cross-platform Development:** Consideration for iOS and Android compatibility.
### 8. Success Metrics
* **Increased Chart Screen Usage:** Monitor daily/monthly active users on the primary chart screen.
* **Indicator Adoption Rate:** Track the frequency of technical indicator additions and usage.
* **AI Insights Engagement:** Measure views and interactions with the AI Analysis Insights screen.
* **User Feedback:** Gather qualitative feedback on UI/UX improvements, particularly concerning the professional look and feel.
* **Retention Rate:** Observe improvements in overall user retention post-launch.
### 9. Out-of-Scope (for this initial phase)
* Detailed Order Book and Depth Chart screens.
* Advanced trade execution functionalities (buy/sell orders, order types).
* Price Alert creation screen.
* Portfolio management and wallet functionalities.
* Social trading features.
These items may be considered for future project phases based on user feedback and business priorities.
### 10. Dependencies & Assumptions
* **Design Resources:** Availability of UI/UX designers for detailed mockups, prototypes, and asset creation.
* **Backend Support:** Existing or developed backend APIs to support real-time data feeds and AI analysis computations.
* **Third-party Integrations:** If using a third-party charting library or AI service, ensure licensing and integration capabilities.
* **User Feedback Loop:** Mechanism for collecting and acting on user feedback post-launch.

View File

@ -0,0 +1,293 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>Crypto Dashboard - BTC/USD</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<style data-purpose="base-styles">
body {
background-color: #0d1421; /* Match background color from SCREEN_6 indicator panel */
color: #ffffff;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
}
.text-muted {
color: #8fa2b3; /* Consistent with SCREEN_6 theme */
}
.bg-dark-surface {
background-color: #0d1421;
}
.border-dark {
border-color: #1e293b; /* Deep slate divider color */
}
.bg-card-ai {
background-color: #161e2e; /* AI Analysis theme card background from IMAGE_8 */
border: 1px solid #2d3a4f;
}
</style>
<style data-purpose="chart-customization">
/* Custom grid lines for the chart */
.grid-line {
stroke: #1e293b;
stroke-width: 1;
}
/* Orange color for all candles as requested */
.candle-orange {
fill: #f0b90b;
stroke: #f0b90b;
}
.wick-orange {
stroke: #f0b90b;
stroke-width: 1.5;
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
</head>
<body class="flex flex-col h-screen overflow-hidden">
<!-- BEGIN: Top Navigation Bar (Using COMPONENTS_17 TopAppBar logic) -->
<header class="bg-[#0f131e] fixed top-0 w-full z-50 h-16 px-6 flex items-center justify-between border-b border-[#1b1f2b]">
<div class="flex items-center space-x-3 bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span>
<span class="font-bold text-sm text-[#dfe2f2]">BTC/USD</span>
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">chevron_right</span>
</div>
<div class="flex space-x-1 items-center">
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">1m</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">5m</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">15m</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">1h</button>
<button class="px-2 py-1 text-xs text-[#c3c5d8] hover:bg-[#262a35] transition-colors rounded">4h</button>
<button class="px-2 py-1 text-xs bg-[#2962ff] text-white rounded font-bold">D</button>
</div>
</header>
<!-- BEGIN: Main Content -->
<main class="flex-1 overflow-y-auto pt-16 pb-20">
<!-- BEGIN: Price Statistics -->
<section class="grid grid-cols-4 gap-2 px-4 py-4 border-b border-[#1e293b] bg-[#0d1421]">
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Price</p>
<p class="text-lg font-bold">70339</p>
</div>
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Change <span class="text-red-500"></span></p>
<p class="text-lg font-bold text-red-500">-4.84%</p>
</div>
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">High ↗</p>
<p class="text-lg font-bold">74250</p>
</div>
<div>
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Low ↘</p>
<p class="text-lg font-bold">70279</p>
</div>
</section>
<!-- END: Price Statistics -->
<!-- BEGIN: Candlestick Chart -->
<section class="relative w-full bg-[#0d1421] h-[75vh]" data-purpose="chart-container">
<canvas class="w-full h-full" id="tradingChart"></canvas>
<!-- Price Axis Labels -->
<div class="absolute right-0 top-0 bottom-0 w-16 flex flex-col justify-between text-[10px] text-[#8fa2b3] py-4 pointer-events-none">
<span>77500</span>
<span>75000</span>
<span>72500</span>
<div class="bg-red-500 text-white px-1 py-0.5 rounded-l text-[10px]">70339</div>
<span>67500</span>
<span>65000</span>
<span>62500</span>
</div>
<!-- Time Axis Labels -->
<div class="absolute bottom-2 left-4 right-16 flex justify-between text-[10px] text-[#8fa2b3] pointer-events-none">
<span>01/03 01:00</span>
<span>04/03 01:00</span>
<span>09/03 01:00</span>
<span>14/03 01:00</span>
<span>17/03 01:00</span>
</div>
</section>
<!-- END: Candlestick Chart -->
<!-- BEGIN: Technical Analysis Section -->
<section class="px-4 py-6 bg-[#0d1421]">
<h2 class="text-lg font-bold mb-4 flex items-center gap-2">
<span class="material-symbols-outlined text-[#b6c4ff]">analytics</span>
Technical Analysis
</h2>
<div class="grid grid-cols-2 gap-4">
<!-- Best Moving Averages Card -->
<div class="p-4 rounded-xl bg-card-ai" data-purpose="ta-card">
<p class="text-[10px] font-bold text-[#8fa2b3] uppercase mb-4 tracking-wider">Best Moving Averages</p>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">MA 44</span>
<div class="text-right">
<p class="font-bold text-sm text-[#dfe2f2]">68817.32</p>
<p class="text-[10px] text-green-500">+2.3%</p>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">MA 125</span>
<div class="text-right">
<p class="font-bold text-sm text-[#dfe2f2]">82087.39</p>
<p class="text-[10px] text-red-500">-14.3%</p>
</div>
</div>
</div>
</div>
<!-- Support / Resistance Card -->
<div class="p-4 rounded-xl bg-card-ai" data-purpose="ta-card">
<p class="text-[10px] font-bold text-[#8fa2b3] uppercase mb-4 tracking-wider">Support / Resistance</p>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">Resistance</span>
<span class="font-bold text-sm text-right text-[#dfe2f2]">75972</span>
</div>
<div class="flex justify-between items-center">
<span class="text-[#dfe2f2] text-sm font-medium">Support</span>
<span class="font-bold text-sm text-right text-[#dfe2f2]">62983</span>
</div>
</div>
</div>
</div>
</section>
<!-- END: Technical Analysis Section -->
</main>
<!-- END: Main Content -->
<!-- BEGIN: Bottom Tab Navigation (Using COMPONENTS_17 BottomNavBar logic) -->
<nav class="fixed bottom-0 w-full bg-[#0f131e] border-t border-[#434656]/15 flex justify-around items-center h-16 z-50 px-2 shadow-[0px_-4px_12px_rgba(0,0,0,0.3)]">
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">show_chart</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Markets</span>
</div>
<div class="flex flex-col items-center justify-center text-[#2962ff] dark:text-[#b6c4ff] bg-[#2962ff]/10 rounded-xl px-3 py-1 transition-transform duration-200">
<span class="material-symbols-outlined">candlestick_chart</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Chart</span>
</div>
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">query_stats</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Indicators</span>
</div>
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">analytics</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">Analysis</span>
</div>
<div class="flex flex-col items-center justify-center text-[#c3c5d8] hover:text-[#dfe2f2] transition-transform duration-200">
<span class="material-symbols-outlined">more_horiz</span>
<span class="text-[11px] font-medium font-['Inter'] mt-1">More</span>
</div>
</nav>
<!-- BEGIN: Chart Logic -->
<script data-purpose="chart-rendering"> const canvas = document.getElementById('tradingChart');
const ctx = canvas.getContext('2d');
function drawChart() {
const container = canvas.parentNode;
const rect = container.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
const width = rect.width;
const height = rect.height;
const orange = '#f0b90b';
const gridColor = '#1e293b';
ctx.clearRect(0, 0, width, height);
// Draw Grid
ctx.strokeStyle = gridColor;
ctx.lineWidth = 0.5;
for(let i = 1; i < 8; i++) {
const y = (height / 8) * i;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
for(let i = 1; i < 6; i++) {
const x = (width / 6) * i;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
// Simulated data points [open, high, low, close]
const data = [
[65000, 66000, 64500, 65500],
[65500, 67000, 65000, 66500],
[66500, 66800, 64000, 64500],
[64500, 65000, 63000, 63500],
[63500, 66000, 63000, 65800],
[65800, 67500, 65500, 67000],
[67000, 69000, 66500, 68500],
[68500, 72000, 68000, 71500],
[71500, 73000, 70000, 72500],
[72500, 72500, 70000, 71000],
[71000, 73500, 70500, 73000],
[73000, 74000, 72000, 73500],
[73500, 76500, 73000, 76000],
[76000, 76500, 69000, 70000],
[70000, 71000, 68000, 68500],
[68500, 70000, 67500, 69500],
[69500, 71000, 66000, 67000],
[67000, 68000, 65000, 66000],
[66000, 68500, 65500, 68000],
[68000, 71500, 67500, 71000],
[71000, 72000, 69000, 69500],
[69500, 70500, 68500, 69000],
[69000, 70000, 68000, 69800],
[69800, 71500, 69500, 71200],
[71200, 73000, 71000, 72500],
[72500, 73500, 72000, 73200],
[73200, 74500, 72500, 73800],
[73800, 73800, 70000, 70339]
];
const minPrice = 61000;
const maxPrice = 78000;
const priceToY = (price) => height - ((price - minPrice) / (maxPrice - minPrice)) * height;
const candleWidth = 8;
const gap = 4;
const startX = 20;
data.forEach((d, i) => {
const x = startX + i * (candleWidth + gap);
const openY = priceToY(d[0]);
const highY = priceToY(d[1]);
const lowY = priceToY(d[2]);
const closeY = priceToY(d[3]);
ctx.strokeStyle = orange;
ctx.fillStyle = orange;
ctx.lineWidth = 1;
// Wick
ctx.beginPath();
ctx.moveTo(x + candleWidth / 2, highY);
ctx.lineTo(x + candleWidth / 2, lowY);
ctx.stroke();
// Body
const bodyHeight = Math.abs(openY - closeY);
const bodyY = Math.min(openY, closeY);
ctx.fillRect(x, bodyY, candleWidth, Math.max(bodyHeight, 1));
});
// Horizontal current price line
const currentPriceY = priceToY(70339);
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(239, 68, 68, 0.7)';
ctx.beginPath();
ctx.moveTo(0, currentPriceY);
ctx.lineTo(width, currentPriceY);
ctx.stroke();
ctx.setLineDash([]);
}
window.addEventListener('load', drawChart);
window.addEventListener('resize', drawChart);</script>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -0,0 +1,73 @@
# Design System Strategy: The Nocturnal High-Precision Interface
## 1. Overview & Creative North Star: "The Financial Observatory"
Most trading platforms are cluttered, anxiety-inducing grids of flashing numbers. This design system rejects the "Bloomberg Terminal" chaos in favor of **The Financial Observatory**.
The Creative North Star is **Atmospheric Precision**. We treat the UI not as a flat dashboard, but as a sophisticated, multi-layered digital instrument. By leveraging deep tonal shifts and "glass" depth, we create an environment where the data glows with authority. We move beyond the "standard" dark mode by eliminating harsh borders and using intentional asymmetry to guide the eye toward high-velocity data points.
---
## 2. Colors & Surface Philosophy
The palette is built on a foundation of deep, ink-like navies to reduce eye strain during long trading sessions, accented by high-energy signals.
### Tonal Hierarchy
* **Deep Core (Background):** `#0f131e` (surface-dim). This is the "void" upon which all data sits.
* **The "No-Line" Rule:** We explicitly prohibit 1px solid borders for sectioning. Use background shifts instead.
* *Example:* Place a `surface-container-high` (`#262a35`) panel directly onto a `surface` (`#0f131e`) background. The 4% tonal difference is enough to define the boundary without the "boxed-in" feeling of a line.
* **Surface Nesting:**
* **Level 0 (Background):** `surface` (`#0f131e`)
* **Level 1 (Main Panels):** `surface-container` (`#1b1f2b`)
* **Level 2 (In-Panel Cards/Inputs):** `surface-container-high` (`#262a35`)
* **The Glass & Gradient Rule:** Use `primary-container` (`#2962ff`) with a 15% opacity and a `20px` backdrop-blur for floating modals. This creates a "frosted lens" effect that maintains the user's context of the market moving behind the window.
---
## 3. Typography: Editorial Authority
We use **Inter** as our sole typeface. Its tall x-height provides maximum legibility for dense numerical data.
* **Display Scale (The Pulse):** Use `display-md` (2.75rem) for the primary portfolio balance. It should feel like a headline in a high-end financial magazine—confident and dominant.
* **Title Scale (The Metadata):** `title-sm` (1rem) is the workhorse for asset names (e.g., BTC/USD).
* **Label Scale (The Data):** `label-sm` (0.6875rem) is reserved for micro-data like timestamps or volume. Use `on-surface-variant` (`#c3c5d8`) to keep this information secondary.
* **Intentional Contrast:** Pair a `headline-sm` title with a `label-md` subtitle using `spacing-1` (0.2rem) to create a tight, professional information cluster.
---
## 4. Elevation & Depth: The Layering Principle
We do not use shadows to simulate height; we use light.
* **Ambient Shadows:** For high-priority floating elements (like a "Buy" confirmation), use a diffused shadow: `0px 24px 48px rgba(0, 0, 0, 0.4)`. The shadow color must be the background color, not pure black, to maintain a natural "Nocturnal" feel.
* **The Ghost Border:** If high-contrast accessibility is required, use `outline-variant` (`#434656`) at **15% opacity**. This creates a "suggestion" of a border that guides the eye without cluttering the interface.
* **Signature Textures:** Apply a subtle linear gradient to main Action Buttons—from `primary` (`#b6c4ff`) to `primary-container` (`#2962ff`). This creates a convex "lens" feel that makes the CTA feel tactile and premium.
---
## 5. Components: Precision Instruments
### Buttons
* **Primary (The Execution):** Rounded `DEFAULT` (0.5rem). Background: `primary-container`. Text: `on-primary-container`. Use the gradient texture mentioned above.
* **Tertiary (The Ghost):** No background. Use `primary` text. This is for secondary actions like "View History."
### Input Fields
* **The "Deep Well" Look:** Background: `surface-container-lowest` (`#0a0e19`). No border. Roundness: `sm` (0.25rem). On focus, the background shifts to `surface-container-high`.
* **Error State:** Use `error` (`#ffb4ab`) only for the helper text and a 1px `error_container` "Ghost Border."
### Trading Cards & Lists
* **Strict No-Divider Rule:** Never use a horizontal line to separate assets in a watchlist. Use `spacing-4` (0.9rem) of vertical white space and a subtle background hover state (`surface-bright`) to define the row.
* **Market Chips:** Use `tertiary-container` (`#a46000`) for "Warning" or "Limit Order" states to provide a sophisticated orange that doesn't feel like a cheap "Alert" icon.
### Additional Trading Components
* **The Ticker Tape:** A seamless, edge-to-edge `surface-container-lowest` bar at the top of the UI. Use `label-md` for price movement, utilizing `tertiary` for neutral/warning moves and `primary` for positive growth.
---
## 6. Dos and Donts
### Do
* **Do** use `surface-container-highest` for "Active" states (e.g., a selected tab).
* **Do** allow charts to breathe. Use `spacing-10` (2.25rem) as a minimum margin between a chart and its control panels.
* **Do** use the `full` (9999px) roundness for pill-shaped "Status" indicators (e.g., "Market Open").
### Don't
* **Don't** use pure white (`#FFFFFF`) for body text. Always use `on-surface` (`#dfe2f2`) to prevent retinal burn-in on OLED screens.
* **Don't** use more than one `display-lg` element per screen. It diminishes the "High-End Editorial" hierarchy.
* **Don't** use standard "Success Green." This system relies on the blue-to-orange `primary` and `tertiary` scales to maintain its unique visual signature.

27
test.json Normal file
View File

@ -0,0 +1,27 @@
{
"$schema": "https://opencode.ai/theme.json",
"theme": {
"background": "#09141B",
"text": "#DEB88D",
"textMuted": "#17384C",
"border": "#17384C",
"borderActive": "#1BBCDD",
"primary": "#FCA02F",
"secondary": "#027C9B",
"accent": "#68D4F1",
"error": "#D15123",
"warning": "#FDD39F",
"success": "#50A3B5",
"info": "#1E4950",
"backgroundPanel": "#17384C",
"backgroundElement": "#1E4950",
"diffAdded": "#628D98",
"diffRemoved": "#D48678",
"diffContext": "#17384C",
"diffHunkHeader": "#434B53",
"diffHighlightAdded": "#1BBCDD",
"diffHighlightRemoved": "#D48678",
"diffAddedBg": "#09141B",
"diffRemovedBg": "#09141B"
}
}