Compare commits
58 Commits
e98c25efc4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f9eda9f18b | |||
| e3999e6506 | |||
| c8d9cf9645 | |||
| 9fefd1847a | |||
| 09c7657070 | |||
| 30ac99479f | |||
| 43fa8efda7 | |||
| fd816edc54 | |||
| cb2cd53a8a | |||
| ace4d4f49e | |||
| cdfe8f1a39 | |||
| 7c92aa38be | |||
| 8931d76a43 | |||
| a1564c25a7 | |||
| b2a4a6963d | |||
| 338b1ee895 | |||
| 3575d37764 | |||
| cd2ca2e220 | |||
| eda151bff5 | |||
| 39df199c7f | |||
| a34f80f841 | |||
| 96edde8f81 | |||
| 8fc6c4f047 | |||
| 4a3a4a68ce | |||
| 53ddb02dff | |||
| 7d9a8ea237 | |||
| 1c5404bc8e | |||
| b79b82368d | |||
| ee58df6169 | |||
| 03a1262100 | |||
| 785792fa6e | |||
| 62eeffaf1d | |||
| 0e8bf8e3bd | |||
| de00d603fc | |||
| 5624e3a8b7 | |||
| 5efd652456 | |||
| a9be584c0e | |||
| eedd532ba7 | |||
| 870f7574cc | |||
| b9d3add00d | |||
| e47b9cd5c3 | |||
| 406c3d7b95 | |||
| c7cf662da2 | |||
| dd53b13979 | |||
| cbba89ef0f | |||
| 276ea8bdd5 | |||
| 3d6b500926 | |||
| 79802aa173 | |||
| 8bb61e014d | |||
| cf603d7315 | |||
| b80597dc42 | |||
| 1b6e307a79 | |||
| 15881b7db8 | |||
| 55f61e9b3c | |||
| 11fe986b5b | |||
| d42d5941fc | |||
| 86216cccaf | |||
| 509f8033fa |
166
AGENTS.md
@ -1,166 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## 1. Build, Lint, and Test Commands
|
|
||||||
|
|
||||||
### 1.1 Build
|
|
||||||
- **Development build**: `npm run build:dev`
|
|
||||||
- Compiles source files with source maps enabled.
|
|
||||||
- Output directory: `dist/`.
|
|
||||||
- **Production build**: `npm run build:prod`
|
|
||||||
- Optimized bundle, minified assets, no source maps.
|
|
||||||
- Output directory: `dist/`.
|
|
||||||
- **Watch mode**: `npm run build:watch`
|
|
||||||
- Rebuilds on file changes; useful during active development.
|
|
||||||
|
|
||||||
### 1.2 Lint
|
|
||||||
- **ESLint**: `npm run lint`
|
|
||||||
- Runs ESLint over all `src/**/*.js` and `src/**/*.ts`.
|
|
||||||
- Fails on any error with exit code `1`.
|
|
||||||
- **Prettier**: `npm run format`
|
|
||||||
- Checks for formatting violations without fixing them.
|
|
||||||
- Use `npm run format:fix` to automatically format all files.
|
|
||||||
|
|
||||||
### 1.3 Test
|
|
||||||
- **Full test suite**: `npm test`
|
|
||||||
- Executes all Jest test files (`**/__tests__/**/*.js`).
|
|
||||||
- **Run a single test**: `npm test -- <testFilePath>`
|
|
||||||
- Example: `npm test -- src/__tests__/utils/format.test.js`
|
|
||||||
- Runs only the specified test file, preserving test isolation.
|
|
||||||
- **Coverage**: `npm run coverage`
|
|
||||||
- Generates an Istanbul coverage report in `coverage/`.
|
|
||||||
|
|
||||||
### 1.4 Common npm Scripts (add to `package.json` if missing)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"build:dev": "webpack --mode development",
|
|
||||||
"build:prod": "webpack --mode production",
|
|
||||||
"build:watch": "webpack --watch",
|
|
||||||
"lint": "eslint src/**/*.js",
|
|
||||||
"format": "prettier --check src/**/*.js",
|
|
||||||
"format:fix": "prettier --write src/**/*.js",
|
|
||||||
"test": "jest",
|
|
||||||
"coverage": "jest --coverage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Code Style Guidelines
|
|
||||||
|
|
||||||
### 2.1 File Organization
|
|
||||||
- Keep related components, utilities, and hooks in feature‑specific directories.
|
|
||||||
- Use index files (`index.js`) to re‑export public APIs from each folder.
|
|
||||||
|
|
||||||
### 2.2 Imports
|
|
||||||
- **Named imports only** when possible:
|
|
||||||
```js
|
|
||||||
import { foo, bar } from './utils';
|
|
||||||
```
|
|
||||||
- **Default imports** only for modules that expose a default export:
|
|
||||||
```js
|
|
||||||
import defaultFn from './default';
|
|
||||||
```
|
|
||||||
- **Relative paths** (`./`, `../`) must be used; no root‑path shortcuts.
|
|
||||||
|
|
||||||
### 2.3 Formatting
|
|
||||||
- **Indent**: 2 spaces, no tabs.
|
|
||||||
- **Quotes**: Prefer single quotes (`'`) for strings.
|
|
||||||
- **Semicolons**: Optional but encouraged for consistency.
|
|
||||||
- **Line length**: Limit to 100 characters; wrap when exceeded.
|
|
||||||
- **Trailing commas**: Use in object/array literals.
|
|
||||||
|
|
||||||
### 2.4 Naming Conventions
|
|
||||||
- **Variables / Functions**: `camelCase`.
|
|
||||||
- **Classes**: `PascalCase`.
|
|
||||||
- **Constants**: `UPPER_SNAKE_CASE`.
|
|
||||||
- **File names**: kebab-case for CSS, snake_case for utility modules.
|
|
||||||
- **Test files**: suffix with `.test.js` or `.spec.js`.
|
|
||||||
|
|
||||||
### 2.5 TypeScript (if used)
|
|
||||||
- Enable `strict` mode in `tsconfig.json`.
|
|
||||||
- Prefer `interface` for object shapes; use `type` only for unions or conditional types.
|
|
||||||
- Export types explicitly when they are part of a public API.
|
|
||||||
|
|
||||||
### 2.6 Error Handling
|
|
||||||
- **Synchronous**: Throw descriptive `Error` objects.
|
|
||||||
- **Asynchronous**: Return rejected promises or use `try/catch` with custom error classes.
|
|
||||||
- **Logging**: Use `console.error` for runtime errors; include context (e.g., `error.message + ' at ' + stack`).
|
|
||||||
|
|
||||||
### 2.7 Async/Await
|
|
||||||
- Always handle rejected promises; avoid unhandled promise warnings.
|
|
||||||
- Prefer `await` over `.then()` chains for readability.
|
|
||||||
|
|
||||||
### 2.8 Dependency Management
|
|
||||||
- Keep third‑party imports at the top of the file.
|
|
||||||
- Avoid direct use of global Node APIs (`process`, `__dirname`) unless wrapped in utility functions.
|
|
||||||
- Pin versions in `package.json`; run `npm audit` regularly.
|
|
||||||
|
|
||||||
### 2.9 Testing Conventions
|
|
||||||
- **Arrange**: Setup → Exercise → Verify.
|
|
||||||
- **Describe**: Use `describe` blocks for logical groups.
|
|
||||||
- **It**: Use `it` with a clear, past‑tense description.
|
|
||||||
- **BeforeEach / AfterEach**: Clean up mocks and state.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Project‑Specific Rules
|
|
||||||
|
|
||||||
### 3.1 Cursor Rules
|
|
||||||
- No `.cursor` configuration detected in the repository. If you add a `.cursor` folder, include:
|
|
||||||
- `cursor.json` with `"maxLineLength": 100`
|
|
||||||
- `"preferTabs": false`
|
|
||||||
- `"tabWidth": 2`
|
|
||||||
|
|
||||||
### 3.2 Copilot Instructions
|
|
||||||
- No `.github/copilot-instructions.md` found. If you create one, place:
|
|
||||||
- `rules` section outlining PR checklist.
|
|
||||||
- `codeowners` snippet if needed.
|
|
||||||
|
|
||||||
### 3.3 Commit Message Style
|
|
||||||
- Use Conventional Commits (`feat:`, `fix:`, `docs:`, etc.).
|
|
||||||
- Body should explain **why** the change was made, not just **what**.
|
|
||||||
|
|
||||||
### 3.4 Dependency Updates
|
|
||||||
- Run `npm outdated` before any major version bump.
|
|
||||||
- Update `package-lock.json` and run `npm install` to ensure reproducibility.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Utility Scripts
|
|
||||||
|
|
||||||
### 4.1 Clean Build Artifacts
|
|
||||||
```bash
|
|
||||||
# Remove generated files
|
|
||||||
rm -rf dist/ coverage/ node_modules/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Reset Lint Cache
|
|
||||||
```bash
|
|
||||||
# Clear ESLint cache to force re‑lint
|
|
||||||
rm -rf .eslintcache
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Generate Documentation (if used)
|
|
||||||
```bash
|
|
||||||
# Assuming JSDoc is configured
|
|
||||||
jsdoc -c jsdoc.conf.json -r src/ -o docs/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. FAQ
|
|
||||||
|
|
||||||
**Q:** *How do I run a single test without the whole suite?*
|
|
||||||
**A:** `npm test -- path/to/single.test.js`
|
|
||||||
|
|
||||||
**Q:** *Where should I add new utility functions?*
|
|
||||||
**A:** Place them in `src/utils/` with a descriptive filename and export via `index.js`.
|
|
||||||
|
|
||||||
**Q:** *What is the preferred way to handle environment variables?*
|
|
||||||
**A:** Store them in `.env.local` and access via `process.env.VAR_NAME`. Do not commit `.env*` files.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*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.*
|
|
||||||
76
GEMINI.md
Normal 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.
|
||||||
68
api-server.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const PORT = 8000;
|
||||||
|
const HOST = '20.20.20.20';
|
||||||
|
|
||||||
|
app.get('/api/v1/candles', (req, res) => {
|
||||||
|
// Accept any query parameters to prevent 404s (e.g., start/end used by Hurst indicator)
|
||||||
|
// Return mock candle data regardless of parameters
|
||||||
|
const mockCandles = [
|
||||||
|
{
|
||||||
|
time: Math.floor(Date.now() / 1000) - 86400,
|
||||||
|
open: 27000,
|
||||||
|
high: 27500,
|
||||||
|
low: 26900,
|
||||||
|
close: 27300,
|
||||||
|
volume: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: Math.floor(Date.now() / 1000) - 75600,
|
||||||
|
open: 27300,
|
||||||
|
high: 27800,
|
||||||
|
low: 27200,
|
||||||
|
close: 27700,
|
||||||
|
volume: 1200
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Add dummy 148m candle if interval requested
|
||||||
|
if (req.query.interval === '148m') {
|
||||||
|
mockCandles.push({
|
||||||
|
time: Math.floor(Date.now() / 1000) - 64800,
|
||||||
|
open: 27200,
|
||||||
|
high: 27600,
|
||||||
|
low: 27000,
|
||||||
|
close: 27400,
|
||||||
|
volume: 1100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// For any other interval, still return the base mock data
|
||||||
|
res.json({ candles: mockCandles });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json({ candles: mockCandles });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/v1/stats', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
change_24h: 2.5,
|
||||||
|
high_24h: 28000,
|
||||||
|
low_24h: 26500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/v1/ta', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
trend: { direction: 'up', signal: 'buy' },
|
||||||
|
levels: {
|
||||||
|
resistance: 28000,
|
||||||
|
support: 26500,
|
||||||
|
position_in_range: 60
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, HOST, () => {
|
||||||
|
console.log(`API server listening on http://${HOST}:${PORT}`);
|
||||||
|
});
|
||||||
7
config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Configuration for the BTC Trading Dashboard
|
||||||
|
// This file allows the dashboard to connect to any backend API URL.
|
||||||
|
|
||||||
|
window.APP_CONFIG = {
|
||||||
|
// URL of the backend API
|
||||||
|
API_BASE_URL: 'http://20.20.20.20:8000/api/v1'
|
||||||
|
};
|
||||||
736
css/indicators-new.css
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
NEW INDICATOR PANEL STYLES - Single Panel, TradingView-inspired
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.indicator-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subrbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.indicator-panel::-webkit-scrollbar-thumb {
|
||||||
|
background: #363a44;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.indicator-panel::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Bar */
|
||||||
|
.indicator-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.indicator-search:focus-within {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.search-icon {
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.indicator-search input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.indicator-search input::placeholder {
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
}
|
||||||
|
.search-clear {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.search-clear:hover {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Tabs */
|
||||||
|
.category-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.category-tabs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.category-tab {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.category-tab:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
.category-tab.active {
|
||||||
|
background: rgba(41, 98, 255, 0.1);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Sections */
|
||||||
|
.indicator-section {
|
||||||
|
margin: 8px 12px 12px;
|
||||||
|
}
|
||||||
|
.indicator-section.favorites {
|
||||||
|
background: rgba(41, 98, 255, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.section-title button.clear-all,
|
||||||
|
.section-title button.visibility-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.section-title:hover button.clear-all,
|
||||||
|
.section-title:hover button.visibility-toggle {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.visibility-toggle,
|
||||||
|
.clear-all {
|
||||||
|
background: var(--tv-red);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.visibility-toggle {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.visibility-toggle:hover,
|
||||||
|
.clear-all:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Items */
|
||||||
|
.indicator-item {
|
||||||
|
background: var(--tv-panel-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.indicator-item:hover {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
.indicator-item.favorite {
|
||||||
|
border-color: rgba(41, 98, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0; /* Important for ellipsis */
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.indicator-btn:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
border-color: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.indicator-btn.add:hover {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-presets {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.indicator-presets {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.indicator-desc {
|
||||||
|
display: inline;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Indicator Item */
|
||||||
|
.indicator-item.active {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item.active .indicator-name {
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item.active.expanded {
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
background: rgba(41, 98, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.drag-handle:hover {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn.visible,
|
||||||
|
.indicator-btn.expand,
|
||||||
|
.indicator-btn.favorite {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.indicator-btn.expand.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indicator Config (Expanded) */
|
||||||
|
.indicator-config {
|
||||||
|
border-top: 1px solid var(--tv-border);
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
animation: slideDown 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 1000px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-sections {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.config-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-action-btn {
|
||||||
|
background: var(--tv-blue);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.preset-action-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Config Row */
|
||||||
|
.config-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.config-row label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
.config-row select,
|
||||||
|
.config-row input[type="text"],
|
||||||
|
.config-row input[type="number"] {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.config-row select:focus,
|
||||||
|
.config-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-preset {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.input-with-preset input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.presets-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.presets-btn:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color Picker */
|
||||||
|
.color-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.color-picker input[type="color"] {
|
||||||
|
width: 32px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.color-preview {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range Slider */
|
||||||
|
.config-row input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
accent-color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.config-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--tv-border);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--tv-red);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Results */
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Presets List */
|
||||||
|
.presets-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.preset-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.preset-item:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.preset-item.applied {
|
||||||
|
background: rgba(38, 166, 154, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.preset-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
.preset-delete {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.preset-delete:hover {
|
||||||
|
color: var(--tv-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-presets {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range Value Display */
|
||||||
|
.range-value {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--tv-text);
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preset Indicator Button */
|
||||||
|
.preset-indicator {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.preset-indicator:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
border-color: var(--tv-blue);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.category-tabs {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.category-tab {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row label {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly styles for mobile */
|
||||||
|
@media (hover: none) {
|
||||||
|
.indicator-btn {
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tab {
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-item-main {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme improvements */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.indicator-search {
|
||||||
|
background: #1e222d;
|
||||||
|
}
|
||||||
|
.indicator-item {
|
||||||
|
background: #1e222d;
|
||||||
|
}
|
||||||
|
.indicator-config {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
.indicator-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-config > * {
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for presets list */
|
||||||
|
.presets-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.presets-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--tv-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Tabs */
|
||||||
|
.sidebar-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab.active {
|
||||||
|
background: rgba(41, 98, 255, 0.15);
|
||||||
|
color: var(--tv-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Tab Panels */
|
||||||
|
.sidebar-tab-panel {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed sidebar adjustments */
|
||||||
|
.right-sidebar.collapsed .sidebar-tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strategy Panel Styles */
|
||||||
|
.indicator-checklist {
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--tv-bg);
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.indicator-checklist::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.indicator-checklist::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--tv-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.checklist-item:hover {
|
||||||
|
background: var(--tv-hover);
|
||||||
|
}
|
||||||
|
.checklist-item input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equity-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
margin-top: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--tv-border);
|
||||||
|
background: var(--tv-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group {
|
||||||
|
display: flex;
|
||||||
|
background: var(--tv-hover);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn.active {
|
||||||
|
background: var(--tv-border);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn:hover:not(.active) {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
267
css/refined.css
Normal 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;
|
||||||
|
}
|
||||||
14
doc/CHANGELOG.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
## [v0.1.0] - 2026-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial AGENTS.md documenting build, lint, test commands and code style guidelines.
|
||||||
|
- CHANGELOG.md skeleton with standardized versioned sections.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated AGENTS.md with comprehensive guidelines and command references.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Resolved formatting issues in earlier documentation drafts.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- No content removed in this release.
|
||||||
4
favicon.svg
Normal 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 |
BIN
ignore/indicator_panel issue.PNG
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
ignore/line_settings.PNG
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ignore/menu.PNG
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ignore/text.PNG
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
496
index.html
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
<!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"/>
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="theme-color" content="#0d1421">
|
||||||
|
<title>Crypto Dashboard - BTC/USD</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<script src="https://unpkg.com/lightweight-charts@5.1.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="./css/indicators-new.css">
|
||||||
|
<link rel="stylesheet" href="./css/refined.css">
|
||||||
|
<style>
|
||||||
|
/* Sidebar Transition Logic matching existing JS .collapsed class */
|
||||||
|
#rightSidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
#rightSidebar.collapsed {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Styles in Sidebar */
|
||||||
|
.sidebar-tab {
|
||||||
|
@apply px-4 py-2 text-sm font-medium text-gray-400 hover:text-white transition-colors border-b-2 border-transparent;
|
||||||
|
}
|
||||||
|
.sidebar-tab.active {
|
||||||
|
@apply text-blue-500 border-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for clean UI */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col h-screen overflow-hidden bg-[#0d1421] text-white font-['Inter']">
|
||||||
|
|
||||||
|
<!-- Top Navigation Bar -->
|
||||||
|
<header class="bg-[#0f131e] fixed top-0 w-full z-[60] h-16 px-6 flex items-center justify-between border-b border-[#1b1f2b]">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- Mobile Menu Button -->
|
||||||
|
<button id="mobileMenuBtn" class="md:hidden w-10 h-10 flex items-center justify-center text-[#8fa2b3] hover:text-white hover:bg-[#2d3a4f] rounded-md transition-colors z-50">
|
||||||
|
<span class="material-symbols-outlined text-lg">menu</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Search/Symbol Button -->
|
||||||
|
<div class="hidden md: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>
|
||||||
|
|
||||||
|
<!-- Mobile Search -->
|
||||||
|
<div class="md:hidden flex items-center 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] ml-2">BTC/USD</span>
|
||||||
|
<span class="material-symbols-outlined text-sm text-[#8fa2b3] ml-2">chevron_right</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:flex items-center gap-2 mr-4">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-green-500" id="statusDot"></div>
|
||||||
|
<span class="text-xs text-[#8fa2b3]" id="statusText">Live</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex space-x-1 items-center overflow-x-auto no-scrollbar" id="timeframeContainer">
|
||||||
|
<!-- Timeframes injected by JS -->
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex flex-1 pt-16 overflow-hidden">
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 overflow-y-auto pb-20 md:pb-0 no-scrollbar">
|
||||||
|
<!-- Price Statistics -->
|
||||||
|
<section id="priceHeader" class="grid grid-cols-2 md: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" id="currentPrice">--</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Change</p>
|
||||||
|
<p class="text-lg font-bold" id="priceChange">--</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">High ↗</p>
|
||||||
|
<p class="text-lg font-bold" id="dailyHigh">--</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] text-[#8fa2b3] uppercase font-semibold">Low ↘</p>
|
||||||
|
<p class="text-lg font-bold" id="dailyLow">--</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Chart Container -->
|
||||||
|
<section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper">
|
||||||
|
<div id="chart" class="w-full h-full"></div>
|
||||||
|
|
||||||
|
<!-- Horizontal Drawing Toolbar (Top) -->
|
||||||
|
<div class="absolute top-2 left-1/2 -translate-x-1/2 flex flex-row gap-1 z-30 bg-[#1a2333]/80 backdrop-blur border border-[#2d3a4f] p-1 rounded-md shadow-xl" id="drawingToolbar">
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('trend_line', event)" title="Trend Line">
|
||||||
|
<span class="material-symbols-outlined text-sm">call_split</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('horizontal_line', event)" title="Horizontal Line">
|
||||||
|
<span class="material-symbols-outlined text-sm">swap_horizontal_circle</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('vertical_line', event)" title="Vertical Line">
|
||||||
|
<span class="material-symbols-outlined text-sm">swap_vert</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('rectangle', event)" title="Rectangle">
|
||||||
|
<span class="material-symbols-outlined text-sm">crop_square</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('text', event)" title="Text Label">
|
||||||
|
<span class="material-symbols-outlined text-sm">text_fields</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_up', event)" title="Arrow Up">
|
||||||
|
<span class="material-symbols-outlined text-sm">arrow_upward</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_down', event)" title="Arrow Down">
|
||||||
|
<span class="material-symbols-outlined text-sm">arrow_downward</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('measure', event)" title="Measure">
|
||||||
|
<span class="material-symbols-outlined text-sm">straighten</span>
|
||||||
|
</button>
|
||||||
|
<div class="w-[1px] h-full bg-[#2d3a4f] mx-0.5"></div>
|
||||||
|
<button class="w-8 h-8 text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('clear', event)" title="Clear All">
|
||||||
|
<span class="material-symbols-outlined text-sm">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trendline Settings Panel -->
|
||||||
|
<div id="trendlineSettingsPanel" class="hidden absolute top-14 left-1/2 -translate-x-1/2 bg-[#1a2333] border border-[#2d3a4f] rounded-lg shadow-2xl z-50 w-[280px] p-0 text-sm font-['Inter']">
|
||||||
|
<!-- Drag Handle -->
|
||||||
|
<div id="tlPanelHeader" class="h-8 flex items-center justify-between px-3 border-b border-[#2d3a4f] cursor-move bg-[#1f2937] rounded-t-lg">
|
||||||
|
<span class="text-[10px] uppercase tracking-wider font-bold text-gray-400">Settings</span>
|
||||||
|
<button class="text-gray-500 hover:text-white" onclick="window.toggleTLSettings(false)">
|
||||||
|
<span class="material-symbols-outlined text-sm">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex border-b border-[#2d3a4f] mb-4">
|
||||||
|
<button class="flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium" id="tlTabStyle" onclick="window.switchTLTab('style')">Style</button>
|
||||||
|
<button class="flex-1 py-2 text-center text-gray-400 hover:text-white" id="tlTabText" onclick="window.switchTLTab('text')">Text</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Style Tab Content -->
|
||||||
|
<div id="tlContentStyle">
|
||||||
|
<!-- Color Grid (Line) -->
|
||||||
|
<div class="grid grid-cols-8 gap-1 mb-4" id="tlColorGrid">
|
||||||
|
<!-- Colors injected by JS -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Opacity -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex justify-between text-xs text-gray-400 mb-1">
|
||||||
|
<span>Opacity</span>
|
||||||
|
<span id="tlOpacityValue">100%</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" min="0" max="100" value="100" class="w-full h-1 bg-[#2d3a4f] rounded-lg appearance-none cursor-pointer" id="tlOpacityInput">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thickness -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="text-xs text-gray-400 mb-1 block">Thickness</label>
|
||||||
|
<div class="flex bg-[#0d1421] rounded border border-[#2d3a4f] p-1">
|
||||||
|
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(1)" data-thickness="1"><div class="w-4 h-[1px] bg-white"></div></button>
|
||||||
|
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(2)" data-thickness="2"><div class="w-4 h-[2px] bg-white"></div></button>
|
||||||
|
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(3)" data-thickness="3"><div class="w-4 h-[3px] bg-white"></div></button>
|
||||||
|
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(4)" data-thickness="4"><div class="w-4 h-[4px] bg-white"></div></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Line Style -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="text-xs text-gray-400 mb-1 block">Line style</label>
|
||||||
|
<div class="flex bg-[#0d1421] rounded border border-[#2d3a4f] p-1">
|
||||||
|
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-style-btn" onclick="window.setTLStyle(0)" data-style="0"><div class="w-6 border-b border-white"></div></button>
|
||||||
|
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-style-btn" onclick="window.setTLStyle(1)" data-style="1"><div class="w-6 border-b border-dashed border-white"></div></button>
|
||||||
|
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-style-btn" onclick="window.setTLStyle(2)" data-style="2"><div class="w-6 border-b border-dotted border-white"></div></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Tab Content -->
|
||||||
|
<div id="tlContentText" class="hidden">
|
||||||
|
<div class="flex gap-2 mb-4 relative">
|
||||||
|
<!-- Text Color Button -->
|
||||||
|
<div class="w-8 h-8 rounded border border-[#2d3a4f] cursor-pointer" id="tlTextColorBtn" style="background-color: #2962ff"></div>
|
||||||
|
|
||||||
|
<!-- Text Color Picker Popup -->
|
||||||
|
<div id="tlTextColorPicker" class="hidden absolute top-10 left-0 bg-[#1a2333] border border-[#2d3a4f] rounded shadow-2xl p-2 z-[60] w-[210px]">
|
||||||
|
<div class="grid grid-cols-8 gap-1" id="tlTextColorGrid">
|
||||||
|
<!-- Colors injected by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Font Size -->
|
||||||
|
<select id="tlFontSize" class="bg-[#0d1421] border border-[#2d3a4f] rounded text-xs px-2 h-8 text-white focus:outline-none">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="12">12</option>
|
||||||
|
<option value="14" selected>14</option>
|
||||||
|
<option value="16">16</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
<option value="24">24</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Style Toggles -->
|
||||||
|
<button class="w-8 h-8 border border-[#2d3a4f] rounded flex items-center justify-center hover:bg-[#2d3a4f] data-[active=true]:bg-blue-600" id="tlBoldBtn" onclick="window.toggleTLBold()">B</button>
|
||||||
|
<button class="w-8 h-8 border border-[#2d3a4f] rounded flex items-center justify-center hover:bg-[#2d3a4f] italic data-[active=true]:bg-blue-600" id="tlItalicBtn" onclick="window.toggleTLItalic()">I</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="tlTextInput" class="w-full h-24 bg-[#0d1421] border border-[#2d3a4f] rounded p-2 text-xs text-white focus:border-blue-500 focus:outline-none mb-4" placeholder="Add text"></textarea>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<label class="text-xs text-gray-400">Alignment</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<select id="tlAlignVert" class="bg-[#0d1421] border border-[#2d3a4f] rounded text-xs px-2 h-6 text-white focus:outline-none">
|
||||||
|
<option value="top">Top</option>
|
||||||
|
<option value="middle">Middle</option>
|
||||||
|
<option value="bottom">Bottom</option>
|
||||||
|
</select>
|
||||||
|
<select id="tlAlignHorz" class="bg-[#0d1421] border border-[#2d3a4f] rounded text-xs px-2 h-6 text-white focus:outline-none">
|
||||||
|
<option value="left">Left</option>
|
||||||
|
<option value="center">Center</option>
|
||||||
|
<option value="right">Right</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay Controls -->
|
||||||
|
<div class="absolute bottom-4 right-4 flex gap-2 z-10 opacity-0 hover:opacity-100 transition-opacity" id="priceScaleControls">
|
||||||
|
<button class="w-8 h-8 bg-[#1e222d] border border-[#2d3a4f] text-gray-300 flex items-center justify-center rounded hover:bg-[#2d3a4f] transition-colors shadow-lg" id="btnSettings" title="Settings">
|
||||||
|
<span class="material-symbols-outlined text-sm">settings</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Settings Popup -->
|
||||||
|
<div class="hidden absolute bottom-10 right-0 bg-[#1a2333] border border-[#2d3a4f] rounded-lg py-2 z-50 w-64 shadow-xl text-sm" id="settingsPopup">
|
||||||
|
|
||||||
|
<!-- Reset Scale -->
|
||||||
|
<div class="px-4 py-2 hover:bg-[#252f3f] cursor-pointer flex items-center gap-3" onclick="window.dashboard.chart.timeScale().fitContent()">
|
||||||
|
<span class="material-symbols-outlined text-sm">refresh</span> Reset price scale
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-[#2d3a4f] my-1">
|
||||||
|
|
||||||
|
<!-- Scale Toggles -->
|
||||||
|
<div class="px-4 py-2 hover:bg-[#252f3f] cursor-pointer" onclick="window.toggleScaleOption('autoScale')">
|
||||||
|
<span id="autoScaleCheck">✓</span> Auto (fits data to screen)
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-2 hover:bg-[#252f3f] cursor-pointer" onclick="window.toggleScaleOption('invertScale')">
|
||||||
|
<span id="invertScaleCheck"></span> Invert scale
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-[#2d3a4f] my-1">
|
||||||
|
|
||||||
|
<!-- Scale Modes -->
|
||||||
|
<div class="px-4 py-2 hover:bg-[#252f3f] cursor-pointer" onclick="window.setScaleMode(0)">
|
||||||
|
<span id="modeNormalCheck">✓</span> Regular
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-2 hover:bg-[#252f3f] cursor-pointer" onclick="window.setScaleMode(2)">
|
||||||
|
<span id="modePercentCheck"></span> Percent
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-2 hover:bg-[#252f3f] cursor-pointer" onclick="window.setScaleMode(3)">
|
||||||
|
<span id="modeIndexedCheck"></span> Indexed to 100
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-2 hover:bg-[#252f3f] cursor-pointer" onclick="window.setScaleMode(1)">
|
||||||
|
<span id="modeLogCheck"></span> Logarithmic
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-[#2d3a4f] my-1">
|
||||||
|
|
||||||
|
<!-- Candle Colors -->
|
||||||
|
<div class="px-4 py-2">
|
||||||
|
<label class="text-[10px] text-[#8fa2b3] uppercase tracking-wider mb-1 block">Candle Colors</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="color" id="candleUpColor" value="#ff9800" class="flex-1 h-6 cursor-pointer">
|
||||||
|
<input type="color" id="candleDownColor" value="#ff9800" class="flex-1 h-6 cursor-pointer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decimals -->
|
||||||
|
<div class="px-4 py-2 flex items-center justify-between">
|
||||||
|
<label class="text-sm text-gray-300">Decimals</label>
|
||||||
|
<input type="number" id="priceFormatInput" min="0" max="8" value="2" class="w-16 bg-[#0d1421] border border-[#2d3a4f] text-center rounded text-xs">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Controls -->
|
||||||
|
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-10 opacity-0 hover:opacity-100 transition-opacity" id="navControls">
|
||||||
|
<button class="w-8 h-8 bg-[#1e222d]/80 backdrop-blur border border-[#2d3a4f] text-white rounded-full flex items-center justify-center hover:bg-blue-600 transition-colors" id="navLeft">
|
||||||
|
<span class="material-symbols-outlined text-sm">chevron_left</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 bg-[#1e222d]/80 backdrop-blur border border-[#2d3a4f] text-white rounded-full flex items-center justify-center hover:bg-blue-600 transition-colors" id="navRight">
|
||||||
|
<span class="material-symbols-outlined text-sm">chevron_right</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-8 h-8 bg-[#1e222d]/80 backdrop-blur border border-[#2d3a4f] text-white rounded-full flex items-center justify-center hover:bg-blue-600 transition-colors" id="navRecent">
|
||||||
|
<span class="material-symbols-outlined text-sm">keyboard_double_arrow_right</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Technical Analysis Section -->
|
||||||
|
<section class="px-4 py-6 bg-[#0d1421] min-h-[300px]" id="taPanel">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-lg font-bold flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-[#b6c4ff]">analytics</span>
|
||||||
|
Technical Analysis
|
||||||
|
<span id="taInterval" class="text-[10px] bg-blue-600/20 text-blue-400 px-2 py-0.5 rounded ml-2 border border-blue-600/30">1D</span>
|
||||||
|
</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span id="taLastUpdate" class="text-xs text-gray-600 mr-2 hidden sm:inline-block">--</span>
|
||||||
|
<button class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-3 py-1.5 rounded text-xs font-bold hover:shadow-lg transition-all flex items-center gap-1" id="aiBtn" onclick="window.openAIAnalysis()">
|
||||||
|
<span class="material-symbols-outlined text-sm">smart_toy</span> AI Insight
|
||||||
|
</button>
|
||||||
|
<button class="bg-[#1e222d] border border-[#2d3a4f] text-gray-400 px-3 py-1.5 rounded text-xs hover:text-white hover:border-gray-500 transition-colors" onclick="window.refreshTA()">
|
||||||
|
<span class="material-symbols-outlined text-sm align-bottom">refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="taContent" class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div class="text-gray-500 text-sm p-4 text-center w-full col-span-full">Loading analysis data...</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Right-Side Sidebar Navigation (Desktop/Horizontal) -->
|
||||||
|
<nav class="hidden md:flex w-[72px] bg-[#0f131e] border-l border-[#1b1f2b] flex-col items-center py-4 z-50 shadow-[-4px_0px_12px_rgba(0,0,0,0.2)] overflow-y-auto no-scrollbar">
|
||||||
|
<div class="flex flex-col items-center justify-center w-full py-4 text-[#c3c5d8] hover:text-[#dfe2f2] cursor-pointer transition-colors group">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">show_chart</span>
|
||||||
|
<span class="text-[10px] font-medium font-['Inter'] mt-1 opacity-70 group-hover:opacity-100">Markets</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center w-full py-4 text-[#b6c4ff] bg-[#2962ff]/10 border-r-2 border-[#2962ff] cursor-pointer" onclick="window.hideAllPanels()">
|
||||||
|
<span class="material-symbols-outlined">candlestick_chart</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1">Chart</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center w-full py-4 text-[#c3c5d8] hover:text-[#dfe2f2] cursor-pointer transition-colors group" onclick="toggleSidebarDisplay('indicators')">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">query_stats</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1 opacity-70 group-hover:opacity-100">Indicators</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center w-full py-4 text-[#c3c5d8] hover:text-[#dfe2f2] cursor-pointer transition-colors group" onclick="toggleSidebarDisplay('strategy')">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">analytics</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1 opacity-70 group-hover:opacity-100">Strategy</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center w-full py-4 text-[#c3c5d8] hover:text-[#dfe2f2] cursor-pointer transition-colors group" onclick="window.toggleFullscreen()">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">fullscreen</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1 opacity-70 group-hover:opacity-100">Full</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto flex flex-col items-center justify-center w-full py-4 text-[#c3c5d8] hover:text-[#dfe2f2] cursor-pointer transition-colors group">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">more_horiz</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1 opacity-70 group-hover:opacity-100">More</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Navigation (Mobile Vertical) -->
|
||||||
|
<nav class="fixed bottom-0 w-full bg-[#0f131e] border-t border-[#1e293b] flex md:hidden justify-start items-center h-16 z-50 px-2 shadow-[0px_-4px_12px_rgba(0,0,0,0.5)] overflow-x-auto no-scrollbar">
|
||||||
|
<div class="flex flex-col items-center justify-center min-w-[70px] flex-shrink-0 text-[#c3c5d8] hover:text-[#dfe2f2] hover:scale-105 transition-all duration-200 cursor-pointer group">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">show_chart</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1">Markets</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center min-w-[70px] flex-shrink-0 text-[#b6c4ff] bg-[#2962ff]/10 rounded-xl px-4 py-1.5 transition-transform duration-200 cursor-pointer border border-[#2962ff]/20" onclick="window.hideAllPanels()">
|
||||||
|
<span class="material-symbols-outlined">candlestick_chart</span>
|
||||||
|
<span class="text-[10px] font-bold mt-1">Chart</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center min-w-[70px] flex-shrink-0 text-[#c3c5d8] hover:text-[#dfe2f2] hover:scale-105 transition-all duration-200 cursor-pointer group" onclick="toggleSidebarDisplay('indicators')">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">query_stats</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1">Indicators</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center min-w-[70px] flex-shrink-0 text-[#c3c5d8] hover:text-[#dfe2f2] hover:scale-105 transition-all duration-200 cursor-pointer group" onclick="toggleSidebarDisplay('strategy')">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">analytics</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1">Strategy</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center min-w-[70px] flex-shrink-0 text-[#c3c5d8] hover:text-[#dfe2f2] hover:scale-105 transition-all duration-200 cursor-pointer group" onclick="window.toggleFullscreen()">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">fullscreen</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1">Full</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center min-w-[70px] flex-shrink-0 text-[#c3c5d8] hover:text-[#dfe2f2] hover:scale-105 transition-all duration-200 cursor-pointer group">
|
||||||
|
<span class="material-symbols-outlined group-hover:text-blue-400">more_horiz</span>
|
||||||
|
<span class="text-[10px] font-medium mt-1">More</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Sidebar Overlay (Acts as Modal) -->
|
||||||
|
<div id="rightSidebar" class="collapsed fixed top-[64px] right-0 md:right-[72px] bottom-[64px] md:bottom-0 w-full max-w-[370px] bg-[#1a2333] border-l border-[#2d3a4f] shadow-2xl z-40 flex flex-col">
|
||||||
|
<div class="flex justify-between items-center p-3 border-b border-[#2d3a4f] bg-[#1a2333]">
|
||||||
|
<!-- Hidden tab buttons triggers -->
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button class="sidebar-tab active text-xs uppercase tracking-wider font-bold" data-tab="indicators">Indicators</button>
|
||||||
|
<button class="sidebar-tab text-xs uppercase tracking-wider font-bold" data-tab="strategy">Strategy</button>
|
||||||
|
</div>
|
||||||
|
<button class="text-gray-400 hover:text-white p-1 hover:bg-[#2d3a4f] rounded" onclick="window.hideAllPanels()">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Sidebar Toggle -->
|
||||||
|
<button id="sidebarToggleBtn" class="md:flex hidden w-8 h-8 bg-[#1a2333] border border-[#2d3a4f] text-gray-300 flex items-center justify-center rounded hover:bg-[#2d3a4f] transition-colors" title="Toggle Sidebar">
|
||||||
|
<span class="material-symbols-outlined text-sm">menu</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto p-0 sidebar-content bg-[#0d1421]">
|
||||||
|
<div class="sidebar-tab-panel active h-full" id="tab-indicators">
|
||||||
|
<div id="indicatorPanel" class="p-2">
|
||||||
|
<!-- Indicators content injected here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-tab-panel h-full" id="tab-strategy">
|
||||||
|
<div id="strategyPanel" class="p-2">
|
||||||
|
<!-- Strategy content injected here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Initialization Scripts -->
|
||||||
|
<script>
|
||||||
|
// Indicator Definitions
|
||||||
|
window.AVAILABLE_INDICATORS = [
|
||||||
|
{ type: 'hts', name: 'HTS Trend System', description: 'Fast/Slow MAs of High/Low prices' },
|
||||||
|
{ type: 'sma', name: 'SMA', description: 'Simple Moving Average' },
|
||||||
|
{ type: 'ema', name: 'EMA', description: 'Exponential Moving Average' },
|
||||||
|
{ type: 'rsi', name: 'RSI', description: 'Relative Strength Index' },
|
||||||
|
{ type: 'bb', name: 'Bollinger Bands', description: 'Volatility bands' },
|
||||||
|
{ type: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence' },
|
||||||
|
{ type: 'stoch', name: 'Stochastic', description: 'Stochastic Oscillator' },
|
||||||
|
{ type: 'atr', name: 'ATR', description: 'Average True Range' },
|
||||||
|
{ type: 'hurst', name: 'Hurst Bands', description: 'Cyclic price channels' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sidebar Toggle Logic Bridge
|
||||||
|
window.toggleSidebarDisplay = function(tabName) {
|
||||||
|
const sidebar = document.getElementById('rightSidebar');
|
||||||
|
if (!sidebar) return;
|
||||||
|
|
||||||
|
const isOpen = !sidebar.classList.contains('collapsed');
|
||||||
|
const activeTab = document.querySelector('.sidebar-tab.active');
|
||||||
|
const currentTabName = activeTab ? activeTab.dataset.tab : null;
|
||||||
|
|
||||||
|
// If sidebar is open AND the clicked tab is already active, CLOSE IT.
|
||||||
|
if (isOpen && currentTabName === tabName) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
return; // Done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, OPEN it (remove collapsed) and switch tab
|
||||||
|
sidebar.classList.remove('collapsed');
|
||||||
|
|
||||||
|
if (tabName) {
|
||||||
|
// Find and click the hidden tab button to trigger existing logic
|
||||||
|
const tabBtn = document.querySelector(`.sidebar-tab[data-tab="${tabName}"]`);
|
||||||
|
if (tabBtn) {
|
||||||
|
tabBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.hideAllPanels = function() {
|
||||||
|
const sidebar = document.getElementById('rightSidebar');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleFullscreen = function() {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen().catch(err => {
|
||||||
|
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="./config.js"></script>
|
||||||
|
<script type="module" src="./js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
indicator_panel issue.PNG
Normal file
|
After Width: | Height: | Size: 181 KiB |
94
js/app.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { TradingDashboard, refreshTA, openAIAnalysis } from './ui/chart.js';
|
||||||
|
import { restoreSidebarState, toggleSidebar, initSidebarTabs, restoreSidebarTabState } from './ui/sidebar.js';
|
||||||
|
import {
|
||||||
|
initIndicatorPanel,
|
||||||
|
getActiveIndicators,
|
||||||
|
setActiveIndicators,
|
||||||
|
drawIndicatorsOnChart,
|
||||||
|
addIndicator,
|
||||||
|
removeIndicatorById
|
||||||
|
} from './ui/indicators-panel-new.js';
|
||||||
|
import { initStrategyPanel } from './ui/strategy-panel.js';
|
||||||
|
import { IndicatorRegistry } from './indicators/index.js';
|
||||||
|
import { TimezoneConfig } from './config/timezone.js';
|
||||||
|
|
||||||
|
window.dashboard = null;
|
||||||
|
|
||||||
|
window.toggleSidebar = toggleSidebar;
|
||||||
|
window.refreshTA = refreshTA;
|
||||||
|
window.openAIAnalysis = openAIAnalysis;
|
||||||
|
window.TimezoneConfig = TimezoneConfig;
|
||||||
|
window.renderIndicatorList = function() {
|
||||||
|
// This function is no longer needed for sidebar indicators
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export init function for global access
|
||||||
|
window.initIndicatorPanel = initIndicatorPanel;
|
||||||
|
window.addIndicator = addIndicator;
|
||||||
|
window.toggleIndicator = addIndicator;
|
||||||
|
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
|
||||||
|
window.updateIndicatorCandles = drawIndicatorsOnChart;
|
||||||
|
|
||||||
|
window.IndicatorRegistry = IndicatorRegistry;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
// 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');
|
||||||
|
const settingsBtn = document.getElementById('btnSettings');
|
||||||
|
|
||||||
|
if (timezoneSelect) {
|
||||||
|
timezoneSelect.value = TimezoneConfig.getTimezone();
|
||||||
|
timezoneSelect.addEventListener('change', (e) => {
|
||||||
|
TimezoneConfig.setTimezone(e.target.value);
|
||||||
|
settingsPopup.classList.remove('show');
|
||||||
|
// Redraw chart and indicators
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle settings popup
|
||||||
|
if (settingsBtn && settingsPopup) {
|
||||||
|
settingsBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
settingsPopup.classList.toggle('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsPopup.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
settingsPopup.classList.remove('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dashboard = new TradingDashboard();
|
||||||
|
restoreSidebarState();
|
||||||
|
restoreSidebarTabState();
|
||||||
|
initSidebarTabs();
|
||||||
|
|
||||||
|
// Initialize panels
|
||||||
|
window.initIndicatorPanel();
|
||||||
|
initStrategyPanel();
|
||||||
|
});
|
||||||
76
js/config/timezone.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const TimezoneConfig = {
|
||||||
|
timezone: localStorage.getItem('timezone') || 'Europe/Warsaw',
|
||||||
|
|
||||||
|
availableTimezones: [
|
||||||
|
{ value: 'UTC', label: 'UTC', offset: 0 },
|
||||||
|
{ value: 'Europe/London', label: 'London (GMT/BST)', offset: 0 },
|
||||||
|
{ value: 'Europe/Paris', label: 'Central Europe (CET/CEST)', offset: 1 },
|
||||||
|
{ value: 'Europe/Warsaw', label: 'Warsaw (CET/CEST)', offset: 1 },
|
||||||
|
{ value: 'America/New_York', label: 'New York (EST/EDT)', offset: -5 },
|
||||||
|
{ value: 'America/Chicago', label: 'Chicago (CST/CDT)', offset: -6 },
|
||||||
|
{ value: 'America/Los_Angeles', label: 'Los Angeles (PST/PDT)', offset: -8 },
|
||||||
|
{ value: 'Asia/Tokyo', label: 'Tokyo (JST)', offset: 9 },
|
||||||
|
{ value: 'Asia/Shanghai', label: 'Shanghai (CST)', offset: 8 },
|
||||||
|
{ value: 'Australia/Sydney', label: 'Sydney (AEST/AEDT)', offset: 10 },
|
||||||
|
],
|
||||||
|
|
||||||
|
setTimezone(tz) {
|
||||||
|
this.timezone = tz;
|
||||||
|
localStorage.setItem('timezone', tz);
|
||||||
|
document.dispatchEvent(new CustomEvent('timezone-changed', { detail: tz }));
|
||||||
|
},
|
||||||
|
|
||||||
|
getTimezone() {
|
||||||
|
return this.timezone;
|
||||||
|
},
|
||||||
|
|
||||||
|
getOffsetHours(tz = this.timezone) {
|
||||||
|
const now = new Date();
|
||||||
|
const tzDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));
|
||||||
|
const utcDate = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||||
|
return (tzDate - utcDate) / 3600000;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const tz = this.timezone;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
timeZone: tz,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-GB', options);
|
||||||
|
const parts = formatter.formatToParts(date);
|
||||||
|
const get = (type) => parts.find(p => p.type === type).value;
|
||||||
|
|
||||||
|
return `${get('day')}/${get('month')}/${get('year')} ${get('hour')}:${get('minute')}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTickMark(timestamp) {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
const tz = this.timezone;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
timeZone: tz,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-GB', options);
|
||||||
|
const parts = formatter.formatToParts(date);
|
||||||
|
const get = (type) => parts.find(p => p.type === type).value;
|
||||||
|
|
||||||
|
// If it's exactly midnight, just show the date, otherwise show time too
|
||||||
|
const isMidnight = get('hour') === '00' && get('minute') === '00';
|
||||||
|
if (isMidnight) {
|
||||||
|
return `${get('day')}/${get('month')}/${get('year')}`;
|
||||||
|
}
|
||||||
|
return `${get('day')}/${get('month')} ${get('hour')}:${get('minute')}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TimezoneConfig };
|
||||||
15
js/core/constants.js
Normal file
@ -0,0 +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: '#0d1421',
|
||||||
|
tvPanelBg: '#1a2333',
|
||||||
|
tvBorder: '#2d3a4f',
|
||||||
|
tvText: '#ffffff',
|
||||||
|
tvTextSecondary: '#8fa2b3',
|
||||||
|
tvGreen: '#26d367',
|
||||||
|
tvRed: '#ff4d4d',
|
||||||
|
tvBlue: '#2962ff',
|
||||||
|
tvHover: '#252f3f'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const API_BASE = window.APP_CONFIG?.API_BASE_URL || '/api/v1';
|
||||||
1
js/core/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { INTERVALS, COLORS, API_BASE } from './constants.js';
|
||||||
118
js/indicators/atr.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
// Self-contained ATR indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for ATR
|
||||||
|
function calculateATRSignal(indicator, lastCandle, prevCandle, values) {
|
||||||
|
const atr = values?.atr;
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
|
||||||
|
if (!atr || atr === null || !prevClose) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const atrPercent = atr / close * 100;
|
||||||
|
const priceChange = Math.abs(close - prevClose);
|
||||||
|
const atrRatio = priceChange / atr;
|
||||||
|
|
||||||
|
if (atrRatio > 1.5) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.HOLD,
|
||||||
|
strength: 70,
|
||||||
|
value: atr,
|
||||||
|
reasoning: `High volatility: ATR (${atr.toFixed(2)}, ${atrPercent.toFixed(2)}%)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ATR Indicator class
|
||||||
|
export class ATRIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const period = this.params.period || 14;
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
const tr = new Array(candles.length).fill(0);
|
||||||
|
|
||||||
|
for (let i = 1; i < candles.length; i++) {
|
||||||
|
const h_l = candles[i].high - candles[i].low;
|
||||||
|
const h_pc = Math.abs(candles[i].high - candles[i-1].close);
|
||||||
|
const l_pc = Math.abs(candles[i].low - candles[i-1].close);
|
||||||
|
tr[i] = Math.max(h_l, h_pc, l_pc);
|
||||||
|
}
|
||||||
|
|
||||||
|
let atr = 0;
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 1; i <= period; i++) sum += tr[i];
|
||||||
|
atr = sum / period;
|
||||||
|
results[period] = atr;
|
||||||
|
|
||||||
|
for (let i = period + 1; i < candles.length; i++) {
|
||||||
|
atr = (atr * (period - 1) + tr[i]) / period;
|
||||||
|
results[i] = atr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map(atr => ({ atr }));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'ATR',
|
||||||
|
description: 'Average True Range - measures market volatility',
|
||||||
|
inputs: [{
|
||||||
|
name: 'period',
|
||||||
|
label: 'Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 14,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
description: 'Period for ATR calculation'
|
||||||
|
}],
|
||||||
|
plots: [{
|
||||||
|
id: 'value',
|
||||||
|
color: '#795548',
|
||||||
|
title: 'ATR',
|
||||||
|
lineWidth: 1
|
||||||
|
}],
|
||||||
|
displayMode: 'pane'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateATRSignal };
|
||||||
118
js/indicators/bb.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
// Self-contained Bollinger Bands indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for Bollinger Bands
|
||||||
|
function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
const upper = values?.upper;
|
||||||
|
const lower = values?.lower;
|
||||||
|
const prevUpper = prevValues?.upper;
|
||||||
|
const prevLower = prevValues?.lower;
|
||||||
|
|
||||||
|
if (!upper || !lower || prevUpper === undefined || prevLower === undefined || prevClose === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: Price crosses DOWN through lower band (reversal/bounce play)
|
||||||
|
if (prevClose > prevLower && close <= lower) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 70,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through lower Bollinger Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: Price crosses UP through upper band (overextended play)
|
||||||
|
else if (prevClose < prevUpper && close >= upper) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 70,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed UP through upper Bollinger Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bollinger Bands Indicator class
|
||||||
|
export class BollingerBandsIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const period = this.params.period || 20;
|
||||||
|
const stdDevMult = this.params.stdDev || 2;
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < period; j++) sum += candles[i-j].close;
|
||||||
|
const sma = sum / period;
|
||||||
|
|
||||||
|
let diffSum = 0;
|
||||||
|
for (let j = 0; j < period; j++) diffSum += Math.pow(candles[i-j].close - sma, 2);
|
||||||
|
const stdDev = Math.sqrt(diffSum / period);
|
||||||
|
|
||||||
|
results[i] = {
|
||||||
|
middle: sma,
|
||||||
|
upper: sma + (stdDevMult * stdDev),
|
||||||
|
lower: sma - (stdDevMult * stdDev)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'Bollinger Bands',
|
||||||
|
description: 'Volatility bands around a moving average',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'period', label: 'Period', type: 'number', default: 20, min: 1, max: 100 },
|
||||||
|
{ name: 'stdDev', label: 'Std Dev', type: 'number', default: 2, min: 0.5, max: 5, step: 0.5 }
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'upper', color: '#4caf50', title: 'Upper' },
|
||||||
|
{ id: 'middle', color: '#4caf50', title: 'Middle', lineStyle: 2 },
|
||||||
|
{ id: 'lower', color: '#4caf50', title: 'Lower' }
|
||||||
|
],
|
||||||
|
displayMode: 'overlay'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateBollingerBandsSignal };
|
||||||
255
js/indicators/hts.js
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
// Self-contained HTS Trend System indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MA calculations inline (SMA/EMA/RMA/WMA/VWMA)
|
||||||
|
function calculateSMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i >= period) sum -= candles[i - period][source];
|
||||||
|
if (i >= period - 1) results[i] = sum / period;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 2 / (period + 1);
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let ema = 0;
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
ema = sum / period;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ema = (candles[i][source] - ema) * multiplier + ema;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 1 / period;
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let rma = 0;
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
rma = sum / period;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rma = (candles[i][source] - rma) * multiplier + rma;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
const weightSum = (period * (period + 1)) / 2;
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sum += candles[i - j][source] * (period - j);
|
||||||
|
}
|
||||||
|
results[i] = sum / weightSum;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateVWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sumPV = 0;
|
||||||
|
let sumV = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sumPV += candles[i - j][source] * candles[i - j].volume;
|
||||||
|
sumV += candles[i - j].volume;
|
||||||
|
}
|
||||||
|
results[i] = sumV !== 0 ? sumPV / sumV : null;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MA dispatcher function
|
||||||
|
function getMA(type, candles, period, source = 'close') {
|
||||||
|
switch (type.toUpperCase()) {
|
||||||
|
case 'SMA': return calculateSMA(candles, period, source);
|
||||||
|
case 'EMA': return calculateEMA(candles, period, source);
|
||||||
|
case 'RMA': return calculateRMA(candles, period, source);
|
||||||
|
case 'WMA': return calculateWMA(candles, period, source);
|
||||||
|
case 'VWMA': return calculateVWMA(candles, period, source);
|
||||||
|
default: return calculateSMA(candles, period, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for HTS
|
||||||
|
function calculateHTSSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const slowLow = values?.slowLow;
|
||||||
|
const slowHigh = values?.slowHigh;
|
||||||
|
const prevSlowLow = prevValues?.slowLow;
|
||||||
|
const prevSlowHigh = prevValues?.slowHigh;
|
||||||
|
|
||||||
|
if (!slowLow || !slowHigh || !prevSlowLow || !prevSlowHigh) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
|
||||||
|
if (prevClose === undefined) return null;
|
||||||
|
|
||||||
|
// BUY: Price crosses UP through slow low
|
||||||
|
if (prevClose <= prevSlowLow && close > slowLow) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 85,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed UP through slow low`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: Price crosses DOWN through slow high
|
||||||
|
else if (prevClose >= prevSlowHigh && close < slowHigh) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 85,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through slow high`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTS Indicator class
|
||||||
|
export class HTSIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles, oneMinCandles = null, targetTF = null) {
|
||||||
|
const shortPeriod = this.params.short || 33;
|
||||||
|
const longPeriod = this.params.long || 144;
|
||||||
|
const maType = this.params.maType || 'RMA';
|
||||||
|
const useAutoHTS = this.params.useAutoHTS || false;
|
||||||
|
|
||||||
|
let workingCandles = candles;
|
||||||
|
|
||||||
|
if (useAutoHTS && oneMinCandles && targetTF) {
|
||||||
|
const tfMultipliers = {
|
||||||
|
'5m': 5,
|
||||||
|
'15m': 15,
|
||||||
|
'30m': 30,
|
||||||
|
'37m': 37,
|
||||||
|
'1h': 60,
|
||||||
|
'4h': 240
|
||||||
|
};
|
||||||
|
|
||||||
|
const tfGroup = tfMultipliers[targetTF] || 5;
|
||||||
|
|
||||||
|
const grouped = [];
|
||||||
|
let currentGroup = [];
|
||||||
|
for (let i = 0; i < oneMinCandles.length; i++) {
|
||||||
|
currentGroup.push(oneMinCandles[i]);
|
||||||
|
if (currentGroup.length >= tfGroup) {
|
||||||
|
grouped.push({
|
||||||
|
time: currentGroup[tfGroup - 1].time,
|
||||||
|
open: currentGroup[tfGroup - 1].open,
|
||||||
|
high: currentGroup[tfGroup - 1].high,
|
||||||
|
low: currentGroup[tfGroup - 1].low,
|
||||||
|
close: currentGroup[tfGroup - 1].close,
|
||||||
|
volume: currentGroup[tfGroup - 1].volume
|
||||||
|
});
|
||||||
|
currentGroup = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workingCandles = grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortHigh = getMA(maType, workingCandles, shortPeriod, 'high');
|
||||||
|
const shortLow = getMA(maType, workingCandles, shortPeriod, 'low');
|
||||||
|
const longHigh = getMA(maType, workingCandles, longPeriod, 'high');
|
||||||
|
const longLow = getMA(maType, workingCandles, longPeriod, 'low');
|
||||||
|
|
||||||
|
return workingCandles.map((_, i) => ({
|
||||||
|
fastHigh: shortHigh[i],
|
||||||
|
fastLow: shortLow[i],
|
||||||
|
slowHigh: longHigh[i],
|
||||||
|
slowLow: longLow[i],
|
||||||
|
fastMidpoint: ((shortHigh[i] || 0) + (shortLow[i] || 0)) / 2,
|
||||||
|
slowMidpoint: ((longHigh[i] || 0) + (longLow[i] || 0)) / 2
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
const useAutoHTS = this.params?.useAutoHTS || false;
|
||||||
|
|
||||||
|
const fastLineWidth = useAutoHTS ? 1 : 1;
|
||||||
|
const slowLineWidth = useAutoHTS ? 2 : 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'HTS Trend System',
|
||||||
|
description: 'High/Low Trend System with Fast and Slow MAs',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'short', label: 'Fast Period', type: 'number', default: 33, min: 1, max: 500 },
|
||||||
|
{ name: 'long', label: 'Slow Period', type: 'number', default: 144, min: 1, max: 500 },
|
||||||
|
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'RMA' },
|
||||||
|
{ name: 'useAutoHTS', label: 'Auto HTS (TF/4)', type: 'boolean', default: false }
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'fastHigh', color: '#00bcd4', title: 'Fast High', width: fastLineWidth },
|
||||||
|
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: fastLineWidth },
|
||||||
|
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: slowLineWidth },
|
||||||
|
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: slowLineWidth }
|
||||||
|
],
|
||||||
|
displayMode: 'overlay'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateHTSSignal };
|
||||||
421
js/indicators/hurst.js
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
// Self-contained Hurst Bands indicator
|
||||||
|
// Based on J.M. Hurst's cyclic price channel theory
|
||||||
|
// Using RMA + ATR displacement method
|
||||||
|
|
||||||
|
import { INTERVALS } from '../core/constants.js';
|
||||||
|
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#9e9e9e',
|
||||||
|
sell: '#9e9e9e'
|
||||||
|
};
|
||||||
|
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || 'chart';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
|
||||||
|
if (config.cachedResults === undefined) config.cachedResults = null;
|
||||||
|
if (config.cachedMeta === undefined) config.cachedMeta = null;
|
||||||
|
if (config.cachedTimeframe === undefined) config.cachedTimeframe = null;
|
||||||
|
if (config.isFetching === undefined) config.isFetching = false;
|
||||||
|
if (config.lastProcessedTime === undefined) config.lastProcessedTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cachedResults() { return this.config.cachedResults; }
|
||||||
|
set cachedResults(v) { this.config.cachedResults = v; }
|
||||||
|
get cachedMeta() { return this.config.cachedMeta; }
|
||||||
|
set cachedMeta(v) { this.config.cachedMeta = v; }
|
||||||
|
get cachedTimeframe() { return this.config.cachedTimeframe; }
|
||||||
|
set cachedTimeframe(v) { this.config.cachedTimeframe = v; }
|
||||||
|
get isFetching() { return this.config.isFetching; }
|
||||||
|
set isFetching(v) { this.config.isFetching = v; }
|
||||||
|
get lastProcessedTime() { return this.config.lastProcessedTime; }
|
||||||
|
set lastProcessedTime(v) { this.config.lastProcessedTime = v; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized RMA that can start from a previous state
|
||||||
|
function calculateRMAIncremental(sourceValue, prevRMA, length) {
|
||||||
|
if (prevRMA === null || isNaN(prevRMA)) return sourceValue;
|
||||||
|
const alpha = 1 / length;
|
||||||
|
return alpha * sourceValue + (1 - alpha) * prevRMA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate RMA for a full array with stable initialization
|
||||||
|
function calculateRMA(sourceArray, length) {
|
||||||
|
const rma = new Array(sourceArray.length).fill(null);
|
||||||
|
let sum = 0;
|
||||||
|
const alpha = 1 / length;
|
||||||
|
const smaLength = Math.round(length);
|
||||||
|
|
||||||
|
for (let i = 0; i < sourceArray.length; i++) {
|
||||||
|
if (i < smaLength - 1) {
|
||||||
|
sum += sourceArray[i];
|
||||||
|
} else if (i === smaLength - 1) {
|
||||||
|
sum += sourceArray[i];
|
||||||
|
rma[i] = sum / smaLength;
|
||||||
|
} else {
|
||||||
|
const prevRMA = rma[i - 1];
|
||||||
|
rma[i] = (prevRMA === null || isNaN(prevRMA))
|
||||||
|
? sourceArray[i]
|
||||||
|
: alpha * sourceArray[i] + (1 - alpha) * prevRMA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rma;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
const upper = values?.upper;
|
||||||
|
const lower = values?.lower;
|
||||||
|
const prevUpper = prevValues?.upper;
|
||||||
|
const prevLower = prevValues?.lower;
|
||||||
|
|
||||||
|
if (close === undefined || prevClose === undefined || !upper || !lower || !prevUpper || !prevLower) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: Price crosses DOWN through lower Hurst Band (dip entry)
|
||||||
|
if (prevClose > prevLower && close <= lower) {
|
||||||
|
return {
|
||||||
|
type: 'buy',
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through lower Hurst Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELL: Price crosses DOWN through upper Hurst Band (reversal entry)
|
||||||
|
if (prevClose > prevUpper && close <= upper) {
|
||||||
|
return {
|
||||||
|
type: 'sell',
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through upper Hurst Band`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveTimeframe(params) {
|
||||||
|
return params.timeframe === 'chart' ? window.dashboard?.currentInterval || '1m' : params.timeframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
function intervalToSeconds(interval) {
|
||||||
|
const amount = parseInt(interval);
|
||||||
|
const unit = interval.replace(/[0-9]/g, '');
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case 'm': return amount * 60;
|
||||||
|
case 'h': return amount * 3600;
|
||||||
|
case 'd': return amount * 86400;
|
||||||
|
case 'w': return amount * 604800;
|
||||||
|
case 'M': return amount * 2592000;
|
||||||
|
default: return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCandlesForTimeframe(tf, startTime, endTime) {
|
||||||
|
const url = `${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${tf}&start=${startTime.toISOString()}&end=${endTime.toISOString()}&limit=5000`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to fetch candles for ${tf}:`, response.status, response.statusText);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
// API returns newest first (desc), but indicators need oldest first (asc)
|
||||||
|
// Also convert time to numeric seconds to match targetCandles
|
||||||
|
return (data.candles || []).reverse().map(c => ({
|
||||||
|
...c,
|
||||||
|
time: Math.floor(new Date(c.time).getTime() / 1000)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Robust forward filling for MTF data.
|
||||||
|
* @param {Array} results - MTF results (e.g. 5m)
|
||||||
|
* @param {Array} targetCandles - Chart candles (e.g. 1m)
|
||||||
|
*/
|
||||||
|
function forwardFillResults(results, targetCandles) {
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
return new Array(targetCandles.length).fill(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filled = new Array(targetCandles.length).fill(null);
|
||||||
|
let resIdx = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < targetCandles.length; i++) {
|
||||||
|
const targetTime = targetCandles[i].time;
|
||||||
|
|
||||||
|
// Advance result index while next result time is <= target time
|
||||||
|
while (resIdx < results.length - 1 && results[resIdx + 1].time <= targetTime) {
|
||||||
|
resIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current result is valid for this target time, use it
|
||||||
|
// (result time must be <= target time)
|
||||||
|
if (results[resIdx] && results[resIdx].time <= targetTime) {
|
||||||
|
filled[i] = results[resIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HurstBandsIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
|
||||||
|
if (!this.params.timeframe) this.params.timeframe = 'chart';
|
||||||
|
if (!this.params.markerBuyShape) this.params.markerBuyShape = 'custom';
|
||||||
|
if (!this.params.markerSellShape) this.params.markerSellShape = 'custom';
|
||||||
|
if (!this.params.markerBuyColor) this.params.markerBuyColor = '#9e9e9e';
|
||||||
|
if (!this.params.markerSellColor) this.params.markerSellColor = '#9e9e9e';
|
||||||
|
if (!this.params.markerBuyCustom) this.params.markerBuyCustom = '▲';
|
||||||
|
if (!this.params.markerSellCustom) this.params.markerSellCustom = '▼';
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const effectiveTf = getEffectiveTimeframe(this.params);
|
||||||
|
const lastCandle = candles[candles.length - 1];
|
||||||
|
|
||||||
|
// Case 1: Different timeframe (MTF)
|
||||||
|
if (effectiveTf !== window.dashboard?.currentInterval && this.params.timeframe !== 'chart') {
|
||||||
|
// If we have cached results, try to forward fill them to match the current candle count
|
||||||
|
if (this.cachedResults && this.cachedTimeframe === effectiveTf) {
|
||||||
|
// If results are stale (last result time is behind last candle time), trigger background fetch
|
||||||
|
const lastResult = this.cachedResults[this.cachedResults.length - 1];
|
||||||
|
const needsFetch = !this.isFetching && (!lastResult || lastCandle.time > lastResult.time + (intervalToSeconds(effectiveTf) / 2));
|
||||||
|
|
||||||
|
if (needsFetch) {
|
||||||
|
this._fetchAndCalculateMtf(effectiveTf, candles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If length matches exactly and params haven't changed, return
|
||||||
|
if (this.cachedResults.length === candles.length && !this.shouldRecalculate()) {
|
||||||
|
return this.cachedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If length differs (e.g. new 1m candle but 5m not fetched yet), forward fill
|
||||||
|
const filled = forwardFillResults(this.cachedResults.filter(r => r !== null), candles);
|
||||||
|
this.cachedResults = filled;
|
||||||
|
return filled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
if (!this.isFetching) {
|
||||||
|
this._fetchAndCalculateMtf(effectiveTf, candles);
|
||||||
|
}
|
||||||
|
return new Array(candles.length).fill(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Same timeframe as chart (Incremental or Full)
|
||||||
|
// Check if we can do incremental update
|
||||||
|
if (this.cachedResults &&
|
||||||
|
this.cachedResults.length > 0 &&
|
||||||
|
this.cachedTimeframe === effectiveTf &&
|
||||||
|
!this.shouldRecalculate() &&
|
||||||
|
candles.length >= this.cachedResults.length &&
|
||||||
|
candles[this.cachedResults.length - 1].time === this.cachedResults[this.cachedResults.length - 1].time) {
|
||||||
|
|
||||||
|
// Only calculate new candles
|
||||||
|
if (candles.length > this.cachedResults.length) {
|
||||||
|
const newResults = this._calculateIncremental(candles, this.cachedResults);
|
||||||
|
this.cachedResults = newResults;
|
||||||
|
}
|
||||||
|
return this.cachedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full calculation
|
||||||
|
const results = this._calculateCore(candles);
|
||||||
|
this.cachedTimeframe = effectiveTf;
|
||||||
|
this.updateCachedMeta(this.params);
|
||||||
|
this.cachedResults = results;
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculateCore(candles) {
|
||||||
|
const mcl_t = this.params.period || 30;
|
||||||
|
const mcm = this.params.multiplier || 1.8;
|
||||||
|
|
||||||
|
const mcl = mcl_t / 2;
|
||||||
|
const mcl_2 = Math.round(mcl / 2);
|
||||||
|
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
const closes = candles.map(c => c.close);
|
||||||
|
|
||||||
|
// True Range for ATR
|
||||||
|
const trArray = candles.map((d, i) => {
|
||||||
|
const prevClose = i > 0 ? candles[i - 1].close : null;
|
||||||
|
if (prevClose === null || isNaN(prevClose)) return d.high - d.low;
|
||||||
|
return Math.max(d.high - d.low, Math.abs(d.high - prevClose), Math.abs(d.low - prevClose));
|
||||||
|
});
|
||||||
|
|
||||||
|
const ma_mcl = calculateRMA(closes, mcl);
|
||||||
|
const atr = calculateRMA(trArray, mcl);
|
||||||
|
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
const mcm_off = mcm * (atr[i] || 0);
|
||||||
|
const historicalIndex = i - mcl_2;
|
||||||
|
const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null;
|
||||||
|
const centerLine = (historical_ma === null || isNaN(historical_ma)) ? closes[i] : historical_ma;
|
||||||
|
|
||||||
|
results[i] = {
|
||||||
|
time: candles[i].time,
|
||||||
|
upper: centerLine + mcm_off,
|
||||||
|
lower: centerLine - mcm_off,
|
||||||
|
ma: ma_mcl[i], // Store intermediate state for incremental updates
|
||||||
|
atr: atr[i]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculateIncremental(candles, oldResults) {
|
||||||
|
const mcl_t = this.params.period || 30;
|
||||||
|
const mcm = this.params.multiplier || 1.8;
|
||||||
|
const mcl = mcl_t / 2;
|
||||||
|
const mcl_2 = Math.round(mcl / 2);
|
||||||
|
|
||||||
|
const results = [...oldResults];
|
||||||
|
const startIndex = oldResults.length;
|
||||||
|
|
||||||
|
for (let i = startIndex; i < candles.length; i++) {
|
||||||
|
const close = candles[i].close;
|
||||||
|
const prevClose = candles[i-1].close;
|
||||||
|
const tr = Math.max(candles[i].high - candles[i].low, Math.abs(candles[i].high - prevClose), Math.abs(candles[i].low - prevClose));
|
||||||
|
|
||||||
|
const prevMA = results[i-1]?.ma;
|
||||||
|
const prevATR = results[i-1]?.atr;
|
||||||
|
|
||||||
|
const currentMA = calculateRMAIncremental(close, prevMA, mcl);
|
||||||
|
const currentATR = calculateRMAIncremental(tr, prevATR, mcl);
|
||||||
|
|
||||||
|
// For displaced center line, we still need the MA from i - mcl_2
|
||||||
|
// Since i >= oldResults.length, i - mcl_2 might be in the old results
|
||||||
|
let historical_ma = null;
|
||||||
|
const historicalIndex = i - mcl_2;
|
||||||
|
if (historicalIndex >= 0) {
|
||||||
|
historical_ma = historicalIndex < startIndex ? results[historicalIndex].ma : null; // In this simple incremental, we don't look ahead
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerLine = (historical_ma === null || isNaN(historical_ma)) ? close : historical_ma;
|
||||||
|
const mcm_off = mcm * (currentATR || 0);
|
||||||
|
|
||||||
|
results[i] = {
|
||||||
|
time: candles[i].time,
|
||||||
|
upper: centerLine + mcm_off,
|
||||||
|
lower: centerLine - mcm_off,
|
||||||
|
ma: currentMA,
|
||||||
|
atr: currentATR
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchAndCalculateMtf(effectiveTf, targetCandles) {
|
||||||
|
this.isFetching = true;
|
||||||
|
try {
|
||||||
|
console.log(`[Hurst] Fetching MTF data for ${effectiveTf}...`);
|
||||||
|
const chartData = window.dashboard?.allData?.get(window.dashboard?.currentInterval) || targetCandles;
|
||||||
|
if (!chartData || chartData.length === 0) {
|
||||||
|
console.warn('[Hurst] No chart data available for timeframe fetch');
|
||||||
|
this.isFetching = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate warmup needed (period + half width)
|
||||||
|
const mcl_t = this.params.period || 30;
|
||||||
|
const warmupBars = mcl_t * 2; // Extra buffer
|
||||||
|
const tfSeconds = intervalToSeconds(effectiveTf);
|
||||||
|
const warmupOffsetSeconds = warmupBars * tfSeconds;
|
||||||
|
|
||||||
|
// Candles endpoint expects ISO strings or timestamps.
|
||||||
|
// chartData[0].time is the earliest candle on chart.
|
||||||
|
const startTime = new Date((chartData[0].time - warmupOffsetSeconds) * 1000);
|
||||||
|
const endTime = new Date(chartData[chartData.length - 1].time * 1000);
|
||||||
|
|
||||||
|
const tfCandles = await getCandlesForTimeframe(effectiveTf, startTime, endTime);
|
||||||
|
if (tfCandles.length === 0) {
|
||||||
|
console.warn(`[Hurst] No candles fetched for ${effectiveTf}`);
|
||||||
|
this.isFetching = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Hurst] Fetched ${tfCandles.length} candles for ${effectiveTf}. Calculating...`);
|
||||||
|
const tfResults = this._calculateCore(tfCandles);
|
||||||
|
const finalResults = forwardFillResults(tfResults, targetCandles);
|
||||||
|
|
||||||
|
// Persist results on the config object
|
||||||
|
this.cachedResults = finalResults;
|
||||||
|
this.cachedTimeframe = effectiveTf;
|
||||||
|
this.updateCachedMeta(this.params);
|
||||||
|
|
||||||
|
console.log(`[Hurst] MTF calculation complete for ${effectiveTf}. Triggering redraw.`);
|
||||||
|
|
||||||
|
// Trigger a redraw of the dashboard to show the new data
|
||||||
|
if (window.drawIndicatorsOnChart) {
|
||||||
|
window.drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Hurst] Error in _fetchAndCalculateMtf:', err);
|
||||||
|
} finally {
|
||||||
|
this.isFetching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'Hurst Bands',
|
||||||
|
description: 'Cyclic price channels based on Hurst theory',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: 'timeframe',
|
||||||
|
label: 'Timeframe',
|
||||||
|
type: 'select',
|
||||||
|
default: 'chart',
|
||||||
|
options: ['chart', ...INTERVALS],
|
||||||
|
labels: { chart: '(Main Chart)' }
|
||||||
|
},
|
||||||
|
{ name: 'period', label: 'Hurst Cycle Length (mcl_t)', type: 'number', default: 30, min: 5, max: 200 },
|
||||||
|
{ name: 'multiplier', label: 'Multiplier (mcm)', type: 'number', default: 1.8, min: 0.5, max: 10, step: 0.1 }
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'upper', color: '#808080', title: 'Upper', lineWidth: 1 },
|
||||||
|
{ id: 'lower', color: '#808080', title: 'Lower', lineWidth: 1 }
|
||||||
|
],
|
||||||
|
bands: [
|
||||||
|
{ topId: 'upper', bottomId: 'lower', color: 'rgba(128, 128, 128, 0.05)' }
|
||||||
|
],
|
||||||
|
displayMode: 'overlay'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRecalculate() {
|
||||||
|
const effectiveTf = getEffectiveTimeframe(this.params);
|
||||||
|
return this.cachedTimeframe !== effectiveTf ||
|
||||||
|
(this.cachedMeta && (this.cachedMeta.period !== this.params.period ||
|
||||||
|
this.cachedMeta.multiplier !== this.params.multiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCachedMeta(params) {
|
||||||
|
this.cachedMeta = {
|
||||||
|
period: params.period,
|
||||||
|
multiplier: params.multiplier
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateHurstSignal };
|
||||||
69
js/indicators/index.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Indicator registry and exports for self-contained indicators
|
||||||
|
|
||||||
|
// Import all indicator classes and their signal functions
|
||||||
|
export { MAIndicator, calculateMASignal } from './moving_average.js';
|
||||||
|
export { MACDIndicator, calculateMACDSignal } from './macd.js';
|
||||||
|
export { HTSIndicator, calculateHTSSignal } from './hts.js';
|
||||||
|
export { RSIIndicator, calculateRSISignal } from './rsi.js';
|
||||||
|
export { BollingerBandsIndicator, calculateBollingerBandsSignal } from './bb.js';
|
||||||
|
export { StochasticIndicator, calculateStochSignal } from './stoch.js';
|
||||||
|
export { ATRIndicator, calculateATRSignal } from './atr.js';
|
||||||
|
export { HurstBandsIndicator, calculateHurstSignal } from './hurst.js';
|
||||||
|
|
||||||
|
// Import for registry
|
||||||
|
import { MAIndicator as MAI, calculateMASignal as CMA } from './moving_average.js';
|
||||||
|
import { MACDIndicator as MACDI, calculateMACDSignal as CMC } from './macd.js';
|
||||||
|
import { HTSIndicator as HTSI, calculateHTSSignal as CHTS } from './hts.js';
|
||||||
|
import { RSIIndicator as RSII, calculateRSISignal as CRSI } from './rsi.js';
|
||||||
|
import { BollingerBandsIndicator as BBI, calculateBollingerBandsSignal as CBB } from './bb.js';
|
||||||
|
import { StochasticIndicator as STOCHI, calculateStochSignal as CST } from './stoch.js';
|
||||||
|
import { ATRIndicator as ATRI, calculateATRSignal as CATR } from './atr.js';
|
||||||
|
import { HurstBandsIndicator as HURSTI, calculateHurstSignal as CHURST } from './hurst.js';
|
||||||
|
|
||||||
|
// Signal function registry for easy dispatch
|
||||||
|
export const SignalFunctionRegistry = {
|
||||||
|
ma: CMA,
|
||||||
|
macd: CMC,
|
||||||
|
hts: CHTS,
|
||||||
|
rsi: CRSI,
|
||||||
|
bb: CBB,
|
||||||
|
stoch: CST,
|
||||||
|
atr: CATR,
|
||||||
|
hurst: CHURST
|
||||||
|
};
|
||||||
|
|
||||||
|
// Indicator registry for UI
|
||||||
|
export const IndicatorRegistry = {
|
||||||
|
ma: MAI,
|
||||||
|
macd: MACDI,
|
||||||
|
hts: HTSI,
|
||||||
|
rsi: RSII,
|
||||||
|
bb: BBI,
|
||||||
|
stoch: STOCHI,
|
||||||
|
atr: ATRI,
|
||||||
|
hurst: HURSTI
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available indicators for the UI catalog
|
||||||
|
*/
|
||||||
|
export function getAvailableIndicators() {
|
||||||
|
return Object.entries(IndicatorRegistry).map(([type, IndicatorClass]) => {
|
||||||
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
name: meta.name || type.toUpperCase(),
|
||||||
|
description: meta.description || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get signal function for an indicator type
|
||||||
|
* @param {string} indicatorType - The type of indicator (e.g., 'ma', 'rsi')
|
||||||
|
* @returns {Function|null} The signal calculation function or null if not found
|
||||||
|
*/
|
||||||
|
export function getSignalFunction(indicatorType) {
|
||||||
|
return SignalFunctionRegistry[indicatorType] || null;
|
||||||
|
}
|
||||||
153
js/indicators/macd.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// Self-contained MACD indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EMA calculation inline (needed for MACD)
|
||||||
|
function calculateEMAInline(data, period) {
|
||||||
|
const multiplier = 2 / (period + 1);
|
||||||
|
const ema = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (i < period - 1) {
|
||||||
|
ema.push(null);
|
||||||
|
} else if (i === period - 1) {
|
||||||
|
ema.push(data[i]);
|
||||||
|
} else {
|
||||||
|
ema.push((data[i] - ema[i - 1]) * multiplier + ema[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for MACD
|
||||||
|
function calculateMACDSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const macd = values?.macd;
|
||||||
|
const signal = values?.signal;
|
||||||
|
const prevMacd = prevValues?.macd;
|
||||||
|
const prevSignal = prevValues?.signal;
|
||||||
|
|
||||||
|
if (macd === undefined || macd === null || signal === undefined || signal === null ||
|
||||||
|
prevMacd === undefined || prevMacd === null || prevSignal === undefined || prevSignal === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: MACD crosses UP through Signal line
|
||||||
|
if (prevMacd <= prevSignal && macd > signal) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 80,
|
||||||
|
value: macd,
|
||||||
|
reasoning: `MACD crossed UP through Signal line`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: MACD crosses DOWN through Signal line
|
||||||
|
else if (prevMacd >= prevSignal && macd < signal) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 80,
|
||||||
|
value: macd,
|
||||||
|
reasoning: `MACD crossed DOWN through Signal line`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MACD Indicator class
|
||||||
|
export class MACDIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const fast = this.params.fast || 12;
|
||||||
|
const slow = this.params.slow || 26;
|
||||||
|
const signalPeriod = this.params.signal || 9;
|
||||||
|
|
||||||
|
const closes = candles.map(c => c.close);
|
||||||
|
|
||||||
|
// Use inline EMA calculation instead of MA.ema()
|
||||||
|
const fastEMA = calculateEMAInline(closes, fast);
|
||||||
|
const slowEMA = calculateEMAInline(closes, slow);
|
||||||
|
|
||||||
|
const macdLine = fastEMA.map((f, i) => (f !== null && slowEMA[i] !== null) ? f - slowEMA[i] : null);
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
let ema = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const signalLine = macdLine.map(m => {
|
||||||
|
if (m === null) return null;
|
||||||
|
count++;
|
||||||
|
if (count < signalPeriod) {
|
||||||
|
sum += m;
|
||||||
|
return null;
|
||||||
|
} else if (count === signalPeriod) {
|
||||||
|
sum += m;
|
||||||
|
ema = sum / signalPeriod;
|
||||||
|
return ema;
|
||||||
|
} else {
|
||||||
|
ema = (m - ema) * (2 / (signalPeriod + 1)) + ema;
|
||||||
|
return ema;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return macdLine.map((m, i) => ({
|
||||||
|
macd: m,
|
||||||
|
signal: signalLine[i],
|
||||||
|
histogram: (m !== null && signalLine[i] !== null) ? m - signalLine[i] : null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'MACD',
|
||||||
|
description: 'Moving Average Convergence Divergence - trend & momentum',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'fast', label: 'Fast Period', type: 'number', default: 12 },
|
||||||
|
{ name: 'slow', label: 'Slow Period', type: 'number', default: 26 },
|
||||||
|
{ name: 'signal', label: 'Signal Period', type: 'number', default: 9 }
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'macd', color: '#2196f3', title: 'MACD' },
|
||||||
|
{ id: 'signal', color: '#ff5722', title: 'Signal' },
|
||||||
|
{ id: 'histogram', color: '#607d8b', title: 'Histogram', type: 'histogram' }
|
||||||
|
],
|
||||||
|
displayMode: 'pane'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateMACDSignal };
|
||||||
221
js/indicators/moving_average.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
// Self-contained Moving Average indicator with SMA/EMA/RMA/WMA/VWMA support
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moving Average math (SMA/EMA/RMA/WMA/VWMA)
|
||||||
|
function calculateSMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i >= period) sum -= candles[i - period][source];
|
||||||
|
if (i >= period - 1) results[i] = sum / period;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 2 / (period + 1);
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let ema = 0;
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
ema = sum / period;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ema = (candles[i][source] - ema) * multiplier + ema;
|
||||||
|
results[i] = ema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRMA(candles, period, source = 'close') {
|
||||||
|
const multiplier = 1 / period;
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
let rma = 0;
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
if (i < period) {
|
||||||
|
sum += candles[i][source];
|
||||||
|
if (i === period - 1) {
|
||||||
|
rma = sum / period;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rma = (candles[i][source] - rma) * multiplier + rma;
|
||||||
|
results[i] = rma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
const weightSum = (period * (period + 1)) / 2;
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sum += candles[i - j][source] * (period - j);
|
||||||
|
}
|
||||||
|
results[i] = sum / weightSum;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateVWMA(candles, period, source = 'close') {
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
|
||||||
|
for (let i = period - 1; i < candles.length; i++) {
|
||||||
|
let sumPV = 0;
|
||||||
|
let sumV = 0;
|
||||||
|
for (let j = 0; j < period; j++) {
|
||||||
|
sumPV += candles[i - j][source] * candles[i - j].volume;
|
||||||
|
sumV += candles[i - j].volume;
|
||||||
|
}
|
||||||
|
results[i] = sumV !== 0 ? sumPV / sumV : null;
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for Moving Average
|
||||||
|
function calculateMASignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const close = lastCandle.close;
|
||||||
|
const prevClose = prevCandle?.close;
|
||||||
|
const ma = values?.ma;
|
||||||
|
const prevMa = prevValues?.ma;
|
||||||
|
|
||||||
|
if (!ma && ma !== 0) return null;
|
||||||
|
if (prevClose === undefined || prevMa === undefined || prevMa === null) return null;
|
||||||
|
|
||||||
|
// BUY: Price crosses UP through MA
|
||||||
|
if (prevClose <= prevMa && close > ma) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed UP through MA`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: Price crosses DOWN through MA
|
||||||
|
else if (prevClose >= prevMa && close < ma) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 80,
|
||||||
|
value: close,
|
||||||
|
reasoning: `Price crossed DOWN through MA`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MA Indicator class
|
||||||
|
export class MAIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const maType = (this.params.maType || 'SMA').toLowerCase();
|
||||||
|
const period = this.params.period || 44;
|
||||||
|
|
||||||
|
let maValues;
|
||||||
|
|
||||||
|
switch (maType) {
|
||||||
|
case 'sma':
|
||||||
|
maValues = calculateSMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'ema':
|
||||||
|
maValues = calculateEMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'rma':
|
||||||
|
maValues = calculateRMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'wma':
|
||||||
|
maValues = calculateWMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
case 'vwma':
|
||||||
|
maValues = calculateVWMA(candles, period, this.params.source || 'close');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
maValues = calculateSMA(candles, period, this.params.source || 'close');
|
||||||
|
}
|
||||||
|
|
||||||
|
return maValues.map(ma => ({ ma }));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'MA',
|
||||||
|
description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: 'period',
|
||||||
|
label: 'Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 44,
|
||||||
|
min: 1,
|
||||||
|
max: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maType',
|
||||||
|
label: 'MA Type',
|
||||||
|
type: 'select',
|
||||||
|
options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'],
|
||||||
|
default: 'SMA'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{
|
||||||
|
id: 'ma',
|
||||||
|
color: '#2962ff',
|
||||||
|
title: 'MA',
|
||||||
|
style: 'solid',
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
displayMode: 'overlay'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export signal function for external use
|
||||||
|
export { calculateMASignal };
|
||||||
141
js/indicators/rsi.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
// Self-contained RSI indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for RSI
|
||||||
|
function calculateRSISignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const rsi = values?.rsi;
|
||||||
|
const prevRsi = prevValues?.rsi;
|
||||||
|
const overbought = indicator.params?.overbought || 70;
|
||||||
|
const oversold = indicator.params?.oversold || 30;
|
||||||
|
|
||||||
|
if (rsi === undefined || rsi === null || prevRsi === undefined || prevRsi === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY when RSI crosses UP through oversold level
|
||||||
|
if (prevRsi < oversold && rsi >= oversold) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 75,
|
||||||
|
value: rsi,
|
||||||
|
reasoning: `RSI crossed UP through oversold level (${oversold})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL when RSI crosses DOWN through overbought level
|
||||||
|
else if (prevRsi > overbought && rsi <= overbought) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 75,
|
||||||
|
value: rsi,
|
||||||
|
reasoning: `RSI crossed DOWN through overbought level (${overbought})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSI Indicator class
|
||||||
|
export class RSIIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const period = this.params.period || 14;
|
||||||
|
const overbought = this.params.overbought || 70;
|
||||||
|
const oversold = this.params.oversold || 30;
|
||||||
|
|
||||||
|
// 1. Calculate RSI using RMA (Wilder's Smoothing)
|
||||||
|
let rsiValues = new Array(candles.length).fill(null);
|
||||||
|
let upSum = 0;
|
||||||
|
let downSum = 0;
|
||||||
|
const rmaAlpha = 1 / period;
|
||||||
|
|
||||||
|
for (let i = 1; i < candles.length; i++) {
|
||||||
|
const diff = candles[i].close - candles[i-1].close;
|
||||||
|
const up = diff > 0 ? diff : 0;
|
||||||
|
const down = diff < 0 ? -diff : 0;
|
||||||
|
|
||||||
|
if (i < period) {
|
||||||
|
upSum += up;
|
||||||
|
downSum += down;
|
||||||
|
} else if (i === period) {
|
||||||
|
upSum += up;
|
||||||
|
downSum += down;
|
||||||
|
const avgUp = upSum / period;
|
||||||
|
const avgDown = downSum / period;
|
||||||
|
rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown)));
|
||||||
|
upSum = avgUp;
|
||||||
|
downSum = avgDown;
|
||||||
|
} else {
|
||||||
|
upSum = (up - upSum) * rmaAlpha + upSum;
|
||||||
|
downSum = (down - downSum) * rmaAlpha + downSum;
|
||||||
|
rsiValues[i] = downSum === 0 ? 100 : (upSum === 0 ? 0 : 100 - (100 / (1 + upSum / downSum)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine results
|
||||||
|
return rsiValues.map((rsi, i) => {
|
||||||
|
return {
|
||||||
|
paneBg: 80,
|
||||||
|
rsi: rsi,
|
||||||
|
overboughtBand: overbought,
|
||||||
|
oversoldBand: oversold
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'RSI',
|
||||||
|
description: 'Relative Strength Index',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'period', label: 'RSI Length', type: 'number', default: 14, min: 1, max: 100 },
|
||||||
|
{ name: 'overbought', label: 'Overbought Level', type: 'number', default: 70, min: 50, max: 95 },
|
||||||
|
{ name: 'oversold', label: 'Oversold Level', type: 'number', default: 30, min: 5, max: 50 }
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'rsi', color: '#7E57C2', title: '', style: 'solid', width: 1, lastValueVisible: true },
|
||||||
|
{ id: 'overboughtBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false },
|
||||||
|
{ id: 'oversoldBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }
|
||||||
|
],
|
||||||
|
displayMode: 'pane',
|
||||||
|
paneMin: 0,
|
||||||
|
paneMax: 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateRSISignal };
|
||||||
139
js/indicators/stoch.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// Self-contained Stochastic Oscillator indicator
|
||||||
|
// Includes math, metadata, signal calculation, and base class
|
||||||
|
|
||||||
|
// Signal constants (defined in each indicator file)
|
||||||
|
const SIGNAL_TYPES = {
|
||||||
|
BUY: 'buy',
|
||||||
|
SELL: 'sell',
|
||||||
|
HOLD: 'hold'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIGNAL_COLORS = {
|
||||||
|
buy: '#26a69a',
|
||||||
|
hold: '#787b86',
|
||||||
|
sell: '#ef5350'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base class (inline replacement for BaseIndicator)
|
||||||
|
class BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
this.id = config.id;
|
||||||
|
this.type = config.type;
|
||||||
|
this.name = config.name;
|
||||||
|
this.params = config.params || {};
|
||||||
|
this.timeframe = config.timeframe || '1m';
|
||||||
|
this.series = [];
|
||||||
|
this.visible = config.visible !== false;
|
||||||
|
this.cachedResults = null;
|
||||||
|
this.cachedMeta = null;
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal calculation for Stochastic
|
||||||
|
function calculateStochSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||||
|
const k = values?.k;
|
||||||
|
const d = values?.d;
|
||||||
|
const prevK = prevValues?.k;
|
||||||
|
const prevD = prevValues?.d;
|
||||||
|
const overbought = indicator.params?.overbought || 80;
|
||||||
|
const oversold = indicator.params?.oversold || 20;
|
||||||
|
|
||||||
|
if (k === undefined || d === undefined || prevK === undefined || prevD === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUY: %K crosses UP through %D while both are oversold
|
||||||
|
if (prevK <= prevD && k > d && k < oversold) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.BUY,
|
||||||
|
strength: 80,
|
||||||
|
value: k,
|
||||||
|
reasoning: `Stochastic %K crossed UP through %D in oversold zone`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// SELL: %K crosses DOWN through %D while both are overbought
|
||||||
|
else if (prevK >= prevD && k < d && k > overbought) {
|
||||||
|
return {
|
||||||
|
type: SIGNAL_TYPES.SELL,
|
||||||
|
strength: 80,
|
||||||
|
value: k,
|
||||||
|
reasoning: `Stochastic %K crossed DOWN through %D in overbought zone`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stochastic Oscillator Indicator class
|
||||||
|
export class StochasticIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(candles) {
|
||||||
|
const kPeriod = this.params.kPeriod || 14;
|
||||||
|
const dPeriod = this.params.dPeriod || 3;
|
||||||
|
const results = new Array(candles.length).fill(null);
|
||||||
|
|
||||||
|
const kValues = new Array(candles.length).fill(null);
|
||||||
|
|
||||||
|
for (let i = kPeriod - 1; i < candles.length; i++) {
|
||||||
|
let lowest = Infinity;
|
||||||
|
let highest = -Infinity;
|
||||||
|
for (let j = 0; j < kPeriod; j++) {
|
||||||
|
lowest = Math.min(lowest, candles[i-j].low);
|
||||||
|
highest = Math.max(highest, candles[i-j].high);
|
||||||
|
}
|
||||||
|
const diff = highest - lowest;
|
||||||
|
kValues[i] = diff === 0 ? 50 : ((candles[i].close - lowest) / diff) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = kPeriod + dPeriod - 2; i < candles.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < dPeriod; j++) sum += kValues[i-j];
|
||||||
|
results[i] = { k: kValues[i], d: sum / dPeriod };
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadata() {
|
||||||
|
return {
|
||||||
|
name: 'Stochastic',
|
||||||
|
description: 'Stochastic Oscillator - compares close to high-low range',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: 'kPeriod',
|
||||||
|
label: '%K Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 14,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
description: 'Lookback period for %K calculation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dPeriod',
|
||||||
|
label: '%D Period',
|
||||||
|
type: 'number',
|
||||||
|
default: 3,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
description: 'Smoothing period for %D (SMA of %K)'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plots: [
|
||||||
|
{ id: 'k', color: '#3f51b5', title: '%K', style: 'solid', width: 1 },
|
||||||
|
{ id: 'd', color: '#ff9800', title: '%D', style: 'solid', width: 1 }
|
||||||
|
],
|
||||||
|
displayMode: 'pane',
|
||||||
|
paneMin: 0,
|
||||||
|
paneMax: 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { calculateStochSignal };
|
||||||
9
js/strategies/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const StrategyRegistry = {};
|
||||||
|
|
||||||
|
export function registerStrategy(name, strategyModule) {
|
||||||
|
StrategyRegistry[name] = strategyModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStrategy(name) {
|
||||||
|
return StrategyRegistry[name];
|
||||||
|
}
|
||||||
612
js/strategies/ping-pong.js
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
import { getSignalFunction } from '../indicators/index.js';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ping_pong_settings';
|
||||||
|
|
||||||
|
function getSavedSettings() {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!saved) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PingPongStrategy = {
|
||||||
|
id: 'ping_pong',
|
||||||
|
name: 'Ping-Pong',
|
||||||
|
|
||||||
|
saveSettings: function() {
|
||||||
|
const settings = {
|
||||||
|
startDate: document.getElementById('simStartDate').value,
|
||||||
|
stopDate: document.getElementById('simStopDate').value,
|
||||||
|
contractType: document.getElementById('simContractType').value,
|
||||||
|
direction: document.getElementById('simDirection').value,
|
||||||
|
autoDirection: document.getElementById('simAutoDirection').checked,
|
||||||
|
capital: document.getElementById('simCapital').value,
|
||||||
|
exchangeLeverage: document.getElementById('simExchangeLeverage').value,
|
||||||
|
maxEffectiveLeverage: document.getElementById('simMaxEffectiveLeverage').value,
|
||||||
|
posSize: document.getElementById('simPosSize').value,
|
||||||
|
tp: document.getElementById('simTP').value
|
||||||
|
};
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||||
|
|
||||||
|
const btn = document.getElementById('saveSimSettings');
|
||||||
|
if (btn) {
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = 'Saved!';
|
||||||
|
btn.style.color = '#26a69a';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.style.color = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUI: function(activeIndicators, formatDisplayDate) {
|
||||||
|
const saved = getSavedSettings();
|
||||||
|
|
||||||
|
// Format initial values for display
|
||||||
|
let startDisplay = saved?.startDate || '01/01/2026 00:00';
|
||||||
|
let stopDisplay = saved?.stopDate || '';
|
||||||
|
|
||||||
|
if (startDisplay.includes('T')) {
|
||||||
|
startDisplay = formatDisplayDate(new Date(startDisplay));
|
||||||
|
}
|
||||||
|
if (stopDisplay.includes('T')) {
|
||||||
|
stopDisplay = formatDisplayDate(new Date(stopDisplay));
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderIndicatorChecklist = (prefix) => {
|
||||||
|
if (activeIndicators.length === 0) {
|
||||||
|
return '<div style="padding: 8px; color: var(--tv-text-secondary); font-size: 11px;">No active indicators on chart</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeIndicators.map(ind => `
|
||||||
|
<label class="checklist-item">
|
||||||
|
<input type="checkbox" data-id="${ind.id}" class="sim-${prefix}-check">
|
||||||
|
<span>${ind.name}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoDirChecked = saved?.autoDirection === true;
|
||||||
|
const disableManualStr = autoDirChecked ? 'disabled' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Start Date & Time</label>
|
||||||
|
<input type="text" id="simStartDate" class="sim-input" value="${startDisplay}" placeholder="DD/MM/YYYY HH:MM">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Stop Date & Time (Optional)</label>
|
||||||
|
<input type="text" id="simStopDate" class="sim-input" value="${stopDisplay}" placeholder="DD/MM/YYYY HH:MM">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group" style="background: rgba(38, 166, 154, 0.1); padding: 8px; border-radius: 4px; border: 1px solid rgba(38, 166, 154, 0.2);">
|
||||||
|
<label class="checklist-item" style="margin-bottom: 0;">
|
||||||
|
<input type="checkbox" id="simAutoDirection" ${autoDirChecked ? 'checked' : ''}>
|
||||||
|
<span style="color: #26a69a; font-weight: bold;">Auto-Detect Direction (1D MA44)</span>
|
||||||
|
</label>
|
||||||
|
<div style="font-size: 10px; color: var(--tv-text-secondary); margin-left: 24px; margin-top: 4px;">
|
||||||
|
Price > MA44: LONG (Inverse/BTC Margin)<br>
|
||||||
|
Price < MA44: SHORT (Linear/USDT Margin)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Contract Type (Manual)</label>
|
||||||
|
<select id="simContractType" class="sim-input" ${disableManualStr}>
|
||||||
|
<option value="linear" ${saved?.contractType === 'linear' ? 'selected' : ''}>Linear (USDT-Margined)</option>
|
||||||
|
<option value="inverse" ${saved?.contractType === 'inverse' ? 'selected' : ''}>Inverse (Coin-Margined)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Direction (Manual)</label>
|
||||||
|
<select id="simDirection" class="sim-input" ${disableManualStr}>
|
||||||
|
<option value="long" ${saved?.direction === 'long' ? 'selected' : ''}>Long</option>
|
||||||
|
<option value="short" ${saved?.direction === 'short' ? 'selected' : ''}>Short</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Initial Capital ($)</label>
|
||||||
|
<input type="number" id="simCapital" class="sim-input" value="${saved?.capital || '10000'}" min="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Exchange Leverage (Ping Size Multiplier)</label>
|
||||||
|
<input type="number" id="simExchangeLeverage" class="sim-input" value="${saved?.exchangeLeverage || '1'}" min="1" max="100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Max Effective Leverage (Total Account Cap)</label>
|
||||||
|
<input type="number" id="simMaxEffectiveLeverage" class="sim-input" value="${saved?.maxEffectiveLeverage || '5'}" min="1" max="100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Position Size ($ Margin per Ping)</label>
|
||||||
|
<input type="number" id="simPosSize" class="sim-input" value="${saved?.posSize || '10'}" min="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Take Profit (%)</label>
|
||||||
|
<input type="number" id="simTP" class="sim-input" value="${saved?.tp || '15'}" step="0.1" min="0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||||
|
<label style="margin-bottom: 0;">Open Signal Indicators</label>
|
||||||
|
<button class="action-btn-text" id="saveSimSettings" style="font-size: 10px; color: #00bcd4; background: none; border: none; cursor: pointer; padding: 0;">Save Defaults</button>
|
||||||
|
</div>
|
||||||
|
<div class="indicator-checklist" id="openSignalsList">
|
||||||
|
${renderIndicatorChecklist('open')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Close Signal Indicators (Empty = Accumulation)</label>
|
||||||
|
<div class="indicator-checklist" id="closeSignalsList">
|
||||||
|
${renderIndicatorChecklist('close')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
attachListeners: function() {
|
||||||
|
const autoCheck = document.getElementById('simAutoDirection');
|
||||||
|
const contractSelect = document.getElementById('simContractType');
|
||||||
|
const dirSelect = document.getElementById('simDirection');
|
||||||
|
|
||||||
|
if (autoCheck) {
|
||||||
|
autoCheck.addEventListener('change', (e) => {
|
||||||
|
const isAuto = e.target.checked;
|
||||||
|
contractSelect.disabled = isAuto;
|
||||||
|
dirSelect.disabled = isAuto;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveBtn = document.getElementById('saveSimSettings');
|
||||||
|
if (saveBtn) saveBtn.addEventListener('click', this.saveSettings.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
runSimulation: async function(activeIndicators, displayResultsCallback) {
|
||||||
|
const btn = document.getElementById('runSimulationBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Preparing Data...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startVal = document.getElementById('simStartDate').value;
|
||||||
|
const stopVal = document.getElementById('simStopDate').value;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
startDate: new Date(startVal).getTime() / 1000,
|
||||||
|
stopDate: stopVal ? new Date(stopVal).getTime() / 1000 : Math.floor(Date.now() / 1000),
|
||||||
|
autoDirection: document.getElementById('simAutoDirection').checked,
|
||||||
|
contractType: document.getElementById('simContractType').value,
|
||||||
|
direction: document.getElementById('simDirection').value,
|
||||||
|
capital: parseFloat(document.getElementById('simCapital').value),
|
||||||
|
exchangeLeverage: parseFloat(document.getElementById('simExchangeLeverage').value),
|
||||||
|
maxEffectiveLeverage: parseFloat(document.getElementById('simMaxEffectiveLeverage').value),
|
||||||
|
posSize: parseFloat(document.getElementById('simPosSize').value),
|
||||||
|
tp: parseFloat(document.getElementById('simTP').value) / 100,
|
||||||
|
openIndicators: Array.from(document.querySelectorAll('.sim-open-check:checked')).map(el => el.dataset.id),
|
||||||
|
closeIndicators: Array.from(document.querySelectorAll('.sim-close-check:checked')).map(el => el.dataset.id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.openIndicators.length === 0) {
|
||||||
|
alert('Please choose at least one indicator for opening positions.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = window.dashboard?.currentInterval || '1d';
|
||||||
|
|
||||||
|
// 1. Ensure data is loaded for the range
|
||||||
|
let allCandles = window.dashboard?.allData?.get(interval) || [];
|
||||||
|
|
||||||
|
const earliestInCache = allCandles.length > 0 ? allCandles[0].time : Infinity;
|
||||||
|
const latestInCache = allCandles.length > 0 ? allCandles[allCandles.length - 1].time : -Infinity;
|
||||||
|
|
||||||
|
if (config.startDate < earliestInCache || config.stopDate > latestInCache) {
|
||||||
|
btn.textContent = 'Fetching from Server...';
|
||||||
|
|
||||||
|
let currentEndISO = new Date(config.stopDate * 1000).toISOString();
|
||||||
|
const startISO = new Date(config.startDate * 1000).toISOString();
|
||||||
|
let keepFetching = true;
|
||||||
|
let newCandlesAdded = false;
|
||||||
|
|
||||||
|
while (keepFetching) {
|
||||||
|
const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${interval}&start=${startISO}&end=${currentEndISO}&limit=10000`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.candles && data.candles.length > 0) {
|
||||||
|
const fetchedCandles = data.candles.reverse().map(c => ({
|
||||||
|
time: Math.floor(new Date(c.time).getTime() / 1000),
|
||||||
|
open: parseFloat(c.open),
|
||||||
|
high: parseFloat(c.high),
|
||||||
|
low: parseFloat(c.low),
|
||||||
|
close: parseFloat(c.close),
|
||||||
|
volume: parseFloat(c.volume || 0)
|
||||||
|
}));
|
||||||
|
|
||||||
|
allCandles = window.dashboard.mergeData(allCandles, fetchedCandles);
|
||||||
|
newCandlesAdded = true;
|
||||||
|
|
||||||
|
// If we received 10000 candles, there might be more. We fetch again using the oldest candle's time - 1s
|
||||||
|
if (data.candles.length === 10000) {
|
||||||
|
const oldestTime = fetchedCandles[0].time;
|
||||||
|
if (oldestTime <= config.startDate) {
|
||||||
|
keepFetching = false;
|
||||||
|
} else {
|
||||||
|
currentEndISO = new Date((oldestTime - 1) * 1000).toISOString();
|
||||||
|
btn.textContent = `Fetching older data... (${new Date(oldestTime * 1000).toLocaleDateString()})`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keepFetching = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keepFetching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newCandlesAdded) {
|
||||||
|
window.dashboard.allData.set(interval, allCandles);
|
||||||
|
window.dashboard.candleSeries.setData(allCandles);
|
||||||
|
|
||||||
|
btn.textContent = 'Calculating Indicators...';
|
||||||
|
window.drawIndicatorsOnChart?.();
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auto-Direction: Fetch 1D candles for MA(44) ---
|
||||||
|
let dailyCandles = [];
|
||||||
|
let dailyMaMap = new Map(); // timestamp (midnight UTC) -> MA44 value
|
||||||
|
|
||||||
|
if (config.autoDirection) {
|
||||||
|
btn.textContent = 'Fetching 1D MA(44)...';
|
||||||
|
// Fetch 1D candles starting 45 days BEFORE the simulation start date to warm up the MA
|
||||||
|
const msPerDay = 24 * 60 * 60 * 1000;
|
||||||
|
const dailyStartISO = new Date((config.startDate * 1000) - (45 * msPerDay)).toISOString();
|
||||||
|
const stopISO = new Date(config.stopDate * 1000).toISOString();
|
||||||
|
|
||||||
|
const dailyResponse = await fetch(`/api/v1/candles?symbol=BTC&interval=1d&start=${dailyStartISO}&end=${stopISO}&limit=5000`);
|
||||||
|
const dailyData = await dailyResponse.json();
|
||||||
|
|
||||||
|
if (dailyData.candles && dailyData.candles.length > 0) {
|
||||||
|
dailyCandles = dailyData.candles.reverse().map(c => ({
|
||||||
|
time: Math.floor(new Date(c.time).getTime() / 1000),
|
||||||
|
close: parseFloat(c.close)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Calculate MA(44)
|
||||||
|
const maPeriod = 44;
|
||||||
|
for (let i = maPeriod - 1; i < dailyCandles.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < maPeriod; j++) {
|
||||||
|
sum += dailyCandles[i - j].close;
|
||||||
|
}
|
||||||
|
const maValue = sum / maPeriod;
|
||||||
|
// Store the MA value using the midnight UTC timestamp of that day
|
||||||
|
dailyMaMap.set(dailyCandles[i].time, maValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[Simulation] Failed to fetch 1D candles for Auto-Direction. Falling back to manual.');
|
||||||
|
config.autoDirection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
btn.textContent = 'Simulating...';
|
||||||
|
|
||||||
|
// Filter candles by the exact range
|
||||||
|
const simCandles = allCandles.filter(c => c.time >= config.startDate && c.time <= config.stopDate);
|
||||||
|
|
||||||
|
if (simCandles.length === 0) {
|
||||||
|
alert('No data available for the selected range.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate indicator signals
|
||||||
|
const indicatorSignals = {};
|
||||||
|
for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) {
|
||||||
|
const ind = activeIndicators.find(a => a.id === indId);
|
||||||
|
if (!ind) continue;
|
||||||
|
|
||||||
|
const signalFunc = getSignalFunction(ind.type);
|
||||||
|
const results = ind.cachedResults;
|
||||||
|
|
||||||
|
if (results && signalFunc) {
|
||||||
|
indicatorSignals[indId] = simCandles.map(candle => {
|
||||||
|
const idx = allCandles.findIndex(c => c.time === candle.time);
|
||||||
|
if (idx < 1) return null;
|
||||||
|
const values = typeof results[idx] === 'object' && results[idx] !== null ? results[idx] : { ma: results[idx] };
|
||||||
|
const prevValues = typeof results[idx-1] === 'object' && results[idx-1] !== null ? results[idx-1] : { ma: results[idx-1] };
|
||||||
|
return signalFunc(ind, allCandles[idx], allCandles[idx-1], values, prevValues);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulation Initial State
|
||||||
|
const startPrice = simCandles[0].open;
|
||||||
|
|
||||||
|
// We maintain a single "walletBalanceUsd" variable as the source of truth for the account size
|
||||||
|
let walletBalanceUsd = config.capital;
|
||||||
|
|
||||||
|
// At any given time, the active margin type determines how we use this balance
|
||||||
|
// When LONG (Inverse), we theoretically buy BTC with it.
|
||||||
|
// When SHORT (Linear), we just use it as USDT.
|
||||||
|
|
||||||
|
// Set initial state based on auto or manual
|
||||||
|
if (config.autoDirection && dailyMaMap.size > 0) {
|
||||||
|
// Find the MA value for the day before start date
|
||||||
|
const simStartDayTime = Math.floor(simCandles[0].time / 86400) * 86400; // Midnight UTC
|
||||||
|
let closestMA = Array.from(dailyMaMap.entries())
|
||||||
|
.filter(([t]) => t <= simStartDayTime)
|
||||||
|
.sort((a,b) => b[0] - a[0])[0];
|
||||||
|
|
||||||
|
if (closestMA) {
|
||||||
|
const price = simCandles[0].open;
|
||||||
|
if (price > closestMA[1]) {
|
||||||
|
config.direction = 'long';
|
||||||
|
config.contractType = 'inverse';
|
||||||
|
} else {
|
||||||
|
config.direction = 'short';
|
||||||
|
config.contractType = 'linear';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let equityData = { usd: [], btc: [] };
|
||||||
|
let totalQty = 0; // Linear: BTC Contracts, Inverse: USD Contracts
|
||||||
|
let avgPrice = 0;
|
||||||
|
let avgPriceData = [];
|
||||||
|
let posSizeData = { btc: [], usd: [] };
|
||||||
|
let trades = [];
|
||||||
|
|
||||||
|
let currentDayStart = Math.floor(simCandles[0].time / 86400) * 86400;
|
||||||
|
|
||||||
|
const PARTIAL_EXIT_PCT = 0.15;
|
||||||
|
const MIN_POSITION_VALUE_USD = 15;
|
||||||
|
|
||||||
|
for (let i = 0; i < simCandles.length; i++) {
|
||||||
|
const candle = simCandles[i];
|
||||||
|
const price = candle.close;
|
||||||
|
let actionTakenInThisCandle = false;
|
||||||
|
|
||||||
|
// --- Auto-Direction Daily Check (Midnight UTC) ---
|
||||||
|
if (config.autoDirection) {
|
||||||
|
const candleDayStart = Math.floor(candle.time / 86400) * 86400;
|
||||||
|
if (candleDayStart > currentDayStart) {
|
||||||
|
currentDayStart = candleDayStart;
|
||||||
|
// It's a new day! Get yesterday's MA(44)
|
||||||
|
let closestMA = Array.from(dailyMaMap.entries())
|
||||||
|
.filter(([t]) => t < currentDayStart)
|
||||||
|
.sort((a,b) => b[0] - a[0])[0];
|
||||||
|
|
||||||
|
if (closestMA) {
|
||||||
|
const maValue = closestMA[1];
|
||||||
|
let newDirection = config.direction;
|
||||||
|
let newContractType = config.contractType;
|
||||||
|
|
||||||
|
if (candle.open > maValue) {
|
||||||
|
newDirection = 'long';
|
||||||
|
newContractType = 'inverse';
|
||||||
|
} else {
|
||||||
|
newDirection = 'short';
|
||||||
|
newContractType = 'linear';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did the trend flip?
|
||||||
|
if (newDirection !== config.direction) {
|
||||||
|
// Force close open position at candle.open (market open)
|
||||||
|
if (totalQty > 0) {
|
||||||
|
let pnlUsd = 0;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
pnlUsd = config.direction === 'long' ? (candle.open - avgPrice) * totalQty : (avgPrice - candle.open) * totalQty;
|
||||||
|
walletBalanceUsd += pnlUsd;
|
||||||
|
} else { // inverse
|
||||||
|
// PnL in BTC, converted back to USD
|
||||||
|
const pnlBtc = config.direction === 'long'
|
||||||
|
? totalQty * (1/avgPrice - 1/candle.open)
|
||||||
|
: totalQty * (1/candle.open - 1/avgPrice);
|
||||||
|
// Inverse margin is BTC, so balance was in BTC.
|
||||||
|
// But we maintain walletBalanceUsd, so we just add the USD value of the PNL
|
||||||
|
pnlUsd = pnlBtc * candle.open;
|
||||||
|
walletBalanceUsd += pnlUsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'exit', time: candle.time,
|
||||||
|
entryPrice: avgPrice, exitPrice: candle.open, pnl: pnlUsd, reason: 'Force Close (Trend Flip)',
|
||||||
|
currentUsd: 0, currentQty: 0
|
||||||
|
});
|
||||||
|
totalQty = 0;
|
||||||
|
avgPrice = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply flip
|
||||||
|
config.direction = newDirection;
|
||||||
|
config.contractType = newContractType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ------------------------------------------------
|
||||||
|
|
||||||
|
// 1. Check TP
|
||||||
|
if (totalQty > 0) {
|
||||||
|
let isTP = false;
|
||||||
|
let exitPrice = price;
|
||||||
|
if (config.direction === 'long') {
|
||||||
|
if (candle.high >= avgPrice * (1 + config.tp)) {
|
||||||
|
isTP = true;
|
||||||
|
exitPrice = avgPrice * (1 + config.tp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (candle.low <= avgPrice * (1 - config.tp)) {
|
||||||
|
isTP = true;
|
||||||
|
exitPrice = avgPrice * (1 - config.tp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTP) {
|
||||||
|
let qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
||||||
|
let remainingQty = totalQty - qtyToClose;
|
||||||
|
let remainingValueUsd = config.contractType === 'linear' ? remainingQty * exitPrice : remainingQty;
|
||||||
|
let reason = 'TP (Partial)';
|
||||||
|
|
||||||
|
if (remainingValueUsd < MIN_POSITION_VALUE_USD) {
|
||||||
|
qtyToClose = totalQty;
|
||||||
|
reason = 'TP (Full - Min Size)';
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnlUsd;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
pnlUsd = config.direction === 'long' ? (exitPrice - avgPrice) * qtyToClose : (avgPrice - exitPrice) * qtyToClose;
|
||||||
|
walletBalanceUsd += pnlUsd;
|
||||||
|
} else {
|
||||||
|
const pnlBtc = config.direction === 'long'
|
||||||
|
? qtyToClose * (1/avgPrice - 1/exitPrice)
|
||||||
|
: qtyToClose * (1/exitPrice - 1/avgPrice);
|
||||||
|
pnlUsd = pnlBtc * exitPrice;
|
||||||
|
walletBalanceUsd += pnlUsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalQty -= qtyToClose;
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'exit', time: candle.time,
|
||||||
|
entryPrice: avgPrice, exitPrice: exitPrice, pnl: pnlUsd, reason: reason,
|
||||||
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
|
});
|
||||||
|
actionTakenInThisCandle = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Close Signals
|
||||||
|
if (!actionTakenInThisCandle && totalQty > 0 && config.closeIndicators.length > 0) {
|
||||||
|
const hasCloseSignal = config.closeIndicators.some(id => {
|
||||||
|
const sig = indicatorSignals[id][i];
|
||||||
|
if (!sig) return false;
|
||||||
|
return config.direction === 'long' ? sig.type === 'sell' : sig.type === 'buy';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasCloseSignal) {
|
||||||
|
let qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
||||||
|
let remainingQty = totalQty - qtyToClose;
|
||||||
|
let remainingValueUsd = config.contractType === 'linear' ? remainingQty * price : remainingQty;
|
||||||
|
let reason = 'Signal (Partial)';
|
||||||
|
|
||||||
|
if (remainingValueUsd < MIN_POSITION_VALUE_USD) {
|
||||||
|
qtyToClose = totalQty;
|
||||||
|
reason = 'Signal (Full - Min Size)';
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnlUsd;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
pnlUsd = config.direction === 'long' ? (price - avgPrice) * qtyToClose : (avgPrice - price) * qtyToClose;
|
||||||
|
walletBalanceUsd += pnlUsd;
|
||||||
|
} else {
|
||||||
|
const pnlBtc = config.direction === 'long'
|
||||||
|
? qtyToClose * (1/avgPrice - 1/price)
|
||||||
|
: qtyToClose * (1/price - 1/avgPrice);
|
||||||
|
pnlUsd = pnlBtc * price;
|
||||||
|
walletBalanceUsd += pnlUsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalQty -= qtyToClose;
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'exit', time: candle.time,
|
||||||
|
entryPrice: avgPrice, exitPrice: price, pnl: pnlUsd, reason: reason,
|
||||||
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
|
});
|
||||||
|
actionTakenInThisCandle = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Current Equity for Margin Check
|
||||||
|
let currentEquityUsd = walletBalanceUsd;
|
||||||
|
if (totalQty > 0) {
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
currentEquityUsd += config.direction === 'long' ? (price - avgPrice) * totalQty : (avgPrice - price) * totalQty;
|
||||||
|
} else {
|
||||||
|
const upnlBtc = config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice);
|
||||||
|
currentEquityUsd += (upnlBtc * price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Open Signals
|
||||||
|
if (!actionTakenInThisCandle) {
|
||||||
|
const hasOpenSignal = config.openIndicators.some(id => {
|
||||||
|
const sig = indicatorSignals[id][i];
|
||||||
|
if (!sig) return false;
|
||||||
|
return config.direction === 'long' ? sig.type === 'buy' : sig.type === 'sell';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasOpenSignal) {
|
||||||
|
const entryValUsd = config.posSize * config.exchangeLeverage;
|
||||||
|
const currentNotionalUsd = config.contractType === 'linear' ? totalQty * price : totalQty;
|
||||||
|
|
||||||
|
const projectedEffectiveLeverage = (currentNotionalUsd + entryValUsd) / Math.max(currentEquityUsd, 0.0000001);
|
||||||
|
|
||||||
|
if (projectedEffectiveLeverage <= config.maxEffectiveLeverage) {
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
const entryQty = entryValUsd / price;
|
||||||
|
avgPrice = ((totalQty * avgPrice) + (entryQty * price)) / (totalQty + entryQty);
|
||||||
|
totalQty += entryQty;
|
||||||
|
} else {
|
||||||
|
avgPrice = (totalQty + entryValUsd) / ((totalQty / avgPrice || 0) + (entryValUsd / price));
|
||||||
|
totalQty += entryValUsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'entry', time: candle.time,
|
||||||
|
entryPrice: price, reason: 'Entry',
|
||||||
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final Equity Recording
|
||||||
|
let finalEquityUsd = walletBalanceUsd;
|
||||||
|
if (totalQty > 0) {
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
finalEquityUsd += config.direction === 'long' ? (price - avgPrice) * totalQty : (avgPrice - price) * totalQty;
|
||||||
|
} else {
|
||||||
|
const upnlBtc = config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice);
|
||||||
|
finalEquityUsd += (upnlBtc * price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let finalEquityBtc = finalEquityUsd / price;
|
||||||
|
|
||||||
|
equityData.usd.push({ time: candle.time, value: finalEquityUsd });
|
||||||
|
equityData.btc.push({ time: candle.time, value: finalEquityBtc });
|
||||||
|
|
||||||
|
if (totalQty > 0.000001) {
|
||||||
|
avgPriceData.push({
|
||||||
|
time: candle.time,
|
||||||
|
value: avgPrice,
|
||||||
|
color: config.direction === 'long' ? '#26a69a' : '#ef5350' // Green for long, Red for short
|
||||||
|
});
|
||||||
|
}
|
||||||
|
posSizeData.btc.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty : totalQty / price });
|
||||||
|
posSizeData.usd.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty * price : totalQty });
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResultsCallback(trades, equityData, config, simCandles[simCandles.length-1].close, avgPriceData, posSizeData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Simulation] Error:', error);
|
||||||
|
alert('Simulation failed.');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Run Simulation';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
1189
js/ui/chart.js
Normal file
1404
js/ui/drawing-tools.js
Normal file
243
js/ui/hts-visualizer.js
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
const HTS_COLORS = {
|
||||||
|
fastHigh: '#00bcd4',
|
||||||
|
fastLow: '#00bcd4',
|
||||||
|
slowHigh: '#f44336',
|
||||||
|
slowLow: '#f44336',
|
||||||
|
bullishZone: 'rgba(38, 166, 154, 0.1)',
|
||||||
|
bearishZone: 'rgba(239, 83, 80, 0.1)',
|
||||||
|
channelRegion: 'rgba(41, 98, 255, 0.05)'
|
||||||
|
};
|
||||||
|
|
||||||
|
let HTSOverlays = [];
|
||||||
|
|
||||||
|
export class HTSVisualizer {
|
||||||
|
constructor(chart, candles) {
|
||||||
|
this.chart = chart;
|
||||||
|
this.candles = candles;
|
||||||
|
this.overlays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.overlays.forEach(overlay => {
|
||||||
|
try {
|
||||||
|
this.chart.removeSeries(overlay.series);
|
||||||
|
} catch (e) { }
|
||||||
|
});
|
||||||
|
this.overlays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addHTSChannels(htsData, isAutoHTS = false) {
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
if (!htsData || htsData.length === 0) return;
|
||||||
|
|
||||||
|
const alpha = isAutoHTS ? 0.3 : 0.3;
|
||||||
|
const lineWidth = isAutoHTS ? 1 : 2;
|
||||||
|
|
||||||
|
const fastHighSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(0, 188, 212, ${alpha})`,
|
||||||
|
lineWidth: lineWidth,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Fast High' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const fastLowSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(0, 188, 212, ${alpha})`,
|
||||||
|
lineWidth: lineWidth,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Fast Low' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const slowHighSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(244, 67, 54, ${alpha})`,
|
||||||
|
lineWidth: lineWidth + 1,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Slow High' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const slowLowSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: `rgba(244, 67, 54, ${alpha})`,
|
||||||
|
lineWidth: lineWidth + 1,
|
||||||
|
lastValueVisible: false,
|
||||||
|
title: 'HTS Slow Low' + (isAutoHTS ? ' (Auto)' : ''),
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const fastHighData = htsData.map(h => ({ time: h.time, value: h.fastHigh }));
|
||||||
|
const fastLowData = htsData.map(h => ({ time: h.time, value: h.fastLow }));
|
||||||
|
const slowHighData = htsData.map(h => ({ time: h.time, value: h.slowHigh }));
|
||||||
|
const slowLowData = htsData.map(h => ({ time: h.time, value: h.slowLow }));
|
||||||
|
|
||||||
|
fastHighSeries.setData(fastHighData);
|
||||||
|
fastLowSeries.setData(fastLowData);
|
||||||
|
slowHighSeries.setData(slowHighData);
|
||||||
|
slowLowSeries.setData(slowLowData);
|
||||||
|
|
||||||
|
this.overlays.push(
|
||||||
|
{ series: fastHighSeries, name: 'fastHigh' },
|
||||||
|
{ series: fastLowSeries, name: 'fastLow' },
|
||||||
|
{ series: slowHighSeries, name: 'slowHigh' },
|
||||||
|
{ series: slowLowSeries, name: 'slowLow' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fastHigh: fastHighSeries,
|
||||||
|
fastLow: fastLowSeries,
|
||||||
|
slowHigh: slowHighSeries,
|
||||||
|
slowLow: slowLowSeries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addTrendZones(htsData) {
|
||||||
|
if (!htsData || htsData.length < 2) return;
|
||||||
|
|
||||||
|
const trendZones = [];
|
||||||
|
let currentZone = null;
|
||||||
|
|
||||||
|
for (let i = 1; i < htsData.length; i++) {
|
||||||
|
const prev = htsData[i - 1];
|
||||||
|
const curr = htsData[i];
|
||||||
|
|
||||||
|
const prevBullish = prev.fastLow > prev.slowLow && prev.fastHigh > prev.slowHigh;
|
||||||
|
const currBullish = curr.fastLow > curr.slowLow && curr.fastHigh > curr.slowHigh;
|
||||||
|
|
||||||
|
const prevBearish = prev.fastLow < prev.slowLow && prev.fastHigh < prev.slowHigh;
|
||||||
|
const currBearish = curr.fastLow < curr.slowLow && curr.fastHigh < curr.slowHigh;
|
||||||
|
|
||||||
|
if (currBullish && !prevBullish) {
|
||||||
|
currentZone = { type: 'bullish', start: curr.time };
|
||||||
|
} else if (currBearish && !prevBearish) {
|
||||||
|
currentZone = { type: 'bearish', start: curr.time };
|
||||||
|
} else if (!currBullish && !currBearish && currentZone) {
|
||||||
|
currentZone.end = prev.time;
|
||||||
|
trendZones.push({ ...currentZone });
|
||||||
|
currentZone = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentZone) {
|
||||||
|
currentZone.end = htsData[htsData.length - 1].time;
|
||||||
|
trendZones.push(currentZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
trendZones.forEach(zone => {
|
||||||
|
const zoneSeries = this.chart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
|
topColor: zone.type === 'bullish' ? 'rgba(38, 166, 154, 0.02)' : 'rgba(239, 83, 80, 0.02)',
|
||||||
|
bottomColor: zone.type === 'bullish' ? 'rgba(38, 166, 154, 0.02)' : 'rgba(239, 83, 80, 0.02)',
|
||||||
|
lineColor: 'transparent',
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.candles && this.candles.length > 0) {
|
||||||
|
const maxPrice = Math.max(...this.candles.map(c => c.high)) * 2;
|
||||||
|
const minPrice = Math.min(...this.candles.map(c => c.low)) * 0.5;
|
||||||
|
|
||||||
|
const startTime = zone.start || (this.candles[0]?.time);
|
||||||
|
const endTime = zone.end || (this.candles[this.candles.length - 1]?.time);
|
||||||
|
|
||||||
|
zoneSeries.setData([
|
||||||
|
{ time: startTime, value: minPrice },
|
||||||
|
{ time: startTime, value: maxPrice },
|
||||||
|
{ time: endTime, value: maxPrice },
|
||||||
|
{ time: endTime, value: minPrice }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.overlays.push({ series: zoneSeries, name: `trendZone_${zone.type}_${zone.start}` });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCrossoverMarkers(htsData) {
|
||||||
|
if (!htsData || htsData.length < 2) return;
|
||||||
|
|
||||||
|
const markers = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < htsData.length; i++) {
|
||||||
|
const prev = htsData[i - 1];
|
||||||
|
const curr = htsData[i];
|
||||||
|
|
||||||
|
if (!prev || !curr) continue;
|
||||||
|
|
||||||
|
const price = curr.price;
|
||||||
|
|
||||||
|
const prevFastLow = prev.fastLow;
|
||||||
|
const currFastLow = curr.fastLow;
|
||||||
|
const prevFastHigh = prev.fastHigh;
|
||||||
|
const currFastHigh = curr.fastHigh;
|
||||||
|
const prevSlowLow = prev.slowLow;
|
||||||
|
const currSlowLow = curr.slowLow;
|
||||||
|
const prevSlowHigh = prev.slowHigh;
|
||||||
|
const currSlowHigh = curr.slowHigh;
|
||||||
|
|
||||||
|
if (prevFastLow <= prevSlowLow && currFastLow > currSlowLow && price > currSlowLow) {
|
||||||
|
markers.push({
|
||||||
|
time: curr.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: '#26a69a',
|
||||||
|
shape: 'arrowUp',
|
||||||
|
text: 'BUY',
|
||||||
|
size: 1.2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevFastHigh >= prevSlowHigh && currFastHigh < currSlowHigh && price < currSlowHigh) {
|
||||||
|
markers.push({
|
||||||
|
time: curr.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: '#ef5350',
|
||||||
|
shape: 'arrowDown',
|
||||||
|
text: 'SELL',
|
||||||
|
size: 1.2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const candleSeries = this.candleData?.series;
|
||||||
|
if (candleSeries) {
|
||||||
|
try {
|
||||||
|
if (typeof candleSeries.setMarkers === 'function') {
|
||||||
|
candleSeries.setMarkers(markers);
|
||||||
|
} else if (typeof SeriesMarkersPrimitive !== 'undefined') {
|
||||||
|
if (!this.markerPrimitive) {
|
||||||
|
this.markerPrimitive = new SeriesMarkersPrimitive();
|
||||||
|
candleSeries.attachPrimitive(this.markerPrimitive);
|
||||||
|
}
|
||||||
|
this.markerPrimitive.setMarkers(markers);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[HTS] Error setting markers:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addHTSVisualization(chart, candleSeries, htsData, candles, isAutoHTS = false) {
|
||||||
|
const visualizer = new HTSVisualizer(chart, candles);
|
||||||
|
visualizer.candleData = { series: candleSeries };
|
||||||
|
visualizer.addHTSChannels(htsData, isAutoHTS);
|
||||||
|
|
||||||
|
// Disable trend zones to avoid visual clutter
|
||||||
|
// visualizer.addTrendZones(htsData);
|
||||||
|
|
||||||
|
if (window.showCrossoverMarkers !== false) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
visualizer.addCrossoverMarkers(htsData);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Crossover markers skipped (API limitation):', e.message);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return visualizer;
|
||||||
|
}
|
||||||
14
js/ui/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export { TradingDashboard, refreshTA, openAIAnalysis } from './chart.js';
|
||||||
|
export { toggleSidebar, restoreSidebarState } from './sidebar.js';
|
||||||
|
export {
|
||||||
|
renderIndicatorList,
|
||||||
|
addNewIndicator,
|
||||||
|
selectIndicator,
|
||||||
|
renderIndicatorConfig,
|
||||||
|
applyIndicatorConfig,
|
||||||
|
removeIndicator,
|
||||||
|
removeIndicatorByIndex,
|
||||||
|
drawIndicatorsOnChart,
|
||||||
|
getActiveIndicators,
|
||||||
|
setActiveIndicators
|
||||||
|
} from './indicators-panel.js';
|
||||||
1412
js/ui/indicators-panel-new.js
Normal file
868
js/ui/indicators-panel-new.js.bak
Normal file
@ -0,0 +1,868 @@
|
|||||||
|
import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
|
||||||
|
|
||||||
|
// State management
|
||||||
|
let activeIndicators = [];
|
||||||
|
let configuringId = null;
|
||||||
|
let searchQuery = '';
|
||||||
|
let selectedCategory = 'all';
|
||||||
|
let nextInstanceId = 1;
|
||||||
|
let listenersAttached = false; // Single flag to track if any listeners are attached
|
||||||
|
|
||||||
|
// Chart pane management
|
||||||
|
let indicatorPanes = new Map();
|
||||||
|
let nextPaneIndex = 1;
|
||||||
|
|
||||||
|
// Presets storage
|
||||||
|
let userPresets = JSON.parse(localStorage.getItem('indicator_presets') || '{}');
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ id: 'all', name: 'All Indicators', icon: '📊' },
|
||||||
|
{ id: 'trend', name: 'Trend', icon: '📊' },
|
||||||
|
{ id: 'momentum', name: 'Momentum', icon: '📈' },
|
||||||
|
{ id: 'volatility', name: 'Volatility', icon: '📉' },
|
||||||
|
{ id: 'volume', name: 'Volume', icon: '🔀' },
|
||||||
|
{ id: 'favorites', name: 'Favorites', icon: '★' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const CATEGORY_MAP = {
|
||||||
|
sma: 'trend', ema: 'trend', hts: 'trend',
|
||||||
|
rsi: 'momentum', macd: 'momentum', stoch: 'momentum',
|
||||||
|
bb: 'volatility', atr: 'volatility',
|
||||||
|
others: 'volume'
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
|
||||||
|
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
|
||||||
|
|
||||||
|
function getDefaultColor(index) {
|
||||||
|
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorCategory(indicator) {
|
||||||
|
return CATEGORY_MAP[indicator.type] || 'trend';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorLabel(indicator) {
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
if (!meta) return indicator.name;
|
||||||
|
|
||||||
|
const paramParts = meta.inputs.map(input => {
|
||||||
|
const val = indicator.params[input.name];
|
||||||
|
if (val !== undefined && val !== input.default) return val;
|
||||||
|
return null;
|
||||||
|
}).filter(v => v !== null);
|
||||||
|
|
||||||
|
if (paramParts.length > 0) {
|
||||||
|
return `${indicator.name} (${paramParts.join(', ')})`;
|
||||||
|
}
|
||||||
|
return indicator.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorMeta(indicator) {
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return null;
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
return instance.getMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupPlotsByColor(plots) {
|
||||||
|
const groups = {};
|
||||||
|
plots.forEach((plot, idx) => {
|
||||||
|
const groupMap = {
|
||||||
|
'fast': 'Fast', 'slow': 'Slow', 'upper': 'Upper', 'lower': 'Lower',
|
||||||
|
'middle': 'Middle', 'basis': 'Middle', 'signal': 'Signal',
|
||||||
|
'histogram': 'Histogram', 'k': '%K', 'd': '%D'
|
||||||
|
};
|
||||||
|
const groupName = Object.entries(groupMap).find(([k, v]) => plot.id.toLowerCase().includes(k))?.[1] || plot.id;
|
||||||
|
if (!groups[groupName]) {
|
||||||
|
groups[groupName] = { name: groupName, indices: [], plots: [] };
|
||||||
|
}
|
||||||
|
groups[groupName].indices.push(idx);
|
||||||
|
groups[groupName].plots.push(plot);
|
||||||
|
});
|
||||||
|
return Object.values(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initIndicatorPanel() {
|
||||||
|
console.log('[IndicatorPanel] Initializing...');
|
||||||
|
renderIndicatorPanel();
|
||||||
|
console.log('[IndicatorPanel] Initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveIndicators() {
|
||||||
|
return activeIndicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveIndicators(indicators) {
|
||||||
|
activeIndicators = indicators;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render main panel
|
||||||
|
export function renderIndicatorPanel() {
|
||||||
|
const container = document.getElementById('indicatorPanel');
|
||||||
|
if (!container) {
|
||||||
|
console.error('[IndicatorPanel] Container #indicatorPanel not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[IndicatorPanel] Rendering panel, searchQuery:', searchQuery, 'selectedCategory:', selectedCategory);
|
||||||
|
|
||||||
|
const available = getAvailableIndicators();
|
||||||
|
const catalog = available.filter(ind => {
|
||||||
|
if (searchQuery && !ind.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
if (selectedCategory === 'all') return true;
|
||||||
|
if (selectedCategory === 'favorites') return false;
|
||||||
|
const cat = CATEGORY_MAP[ind.type] || 'trend';
|
||||||
|
return cat === selectedCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[IndicatorPanel] Total indicators:", available.length, "Filtered to:", catalog.length);
|
||||||
|
|
||||||
|
const favoriteIds = new Set(userPresets.favorites || []);
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="indicator-panel">
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="indicator-search">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="indicatorSearch"
|
||||||
|
placeholder="Search indicators..."
|
||||||
|
value="${searchQuery}"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
${searchQuery ? `<button class="search-clear">×</button>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="category-tabs">
|
||||||
|
${CATEGORIES.map(cat => `
|
||||||
|
<button class="category-tab ${selectedCategory === cat.id ? 'active' : ''}" data-category="${cat.id}">
|
||||||
|
${cat.icon} ${cat.name}
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favorites (if any) -->
|
||||||
|
${[...favoriteIds].length > 0 ? `
|
||||||
|
<div class="indicator-section favorites">
|
||||||
|
<div class="section-title">★ Favorites</div>
|
||||||
|
${[...favoriteIds].map(id => {
|
||||||
|
const ind = available.find(a => {
|
||||||
|
return a.type === id || (activeIndicators.find(ai => ai.id === id)?.type === '');
|
||||||
|
});
|
||||||
|
if (!ind) return '';
|
||||||
|
return renderIndicatorItem(ind, true);
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Active Indicators -->
|
||||||
|
${activeIndicators.length > 0 ? `
|
||||||
|
<div class="indicator-section active">
|
||||||
|
<div class="section-title">
|
||||||
|
${activeIndicators.length} Active
|
||||||
|
${activeIndicators.length > 0 ? `<button class="clear-all">Clear All</button>` : ''}
|
||||||
|
</div>
|
||||||
|
${activeIndicators.slice().reverse().map(ind => renderActiveIndicator(ind)).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Available Indicators -->
|
||||||
|
${catalog.length > 0 ? `
|
||||||
|
<div class="indicator-section catalog">
|
||||||
|
<div class="section-title">Available Indicators</div>
|
||||||
|
${catalog.map(ind => renderIndicatorItem(ind, false)).join('')}
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div class="no-results">
|
||||||
|
No indicators found
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Only setup event listeners once
|
||||||
|
if (!listenersAttached) {
|
||||||
|
setupEventListeners();
|
||||||
|
listenersAttached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorItem(indicator, isFavorite) {
|
||||||
|
const colorDots = '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="indicator-item ${isFavorite ? 'favorite' : ''}" data-type="${indicator.type}">
|
||||||
|
<div class="indicator-item-main">
|
||||||
|
<span class="indicator-name">${indicator.name}</span>
|
||||||
|
<span class="indicator-desc">${indicator.description || ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="indicator-actions">
|
||||||
|
<button class="indicator-btn add" data-type="${indicator.type}" title="Add to chart">+</button>
|
||||||
|
${isFavorite ? '' : `
|
||||||
|
<button class="indicator-btn favorite" data-type="${indicator.type}" title="Add to favorites">
|
||||||
|
${userPresets.favorites?.includes(indicator.type) ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderActiveIndicator(indicator) {
|
||||||
|
const isExpanded = configuringId === indicator.id;
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
const label = getIndicatorLabel(indicator);
|
||||||
|
const isFavorite = userPresets.favorites?.includes(indicator.type) || false;
|
||||||
|
const showPresets = meta.name && function() {
|
||||||
|
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
|
||||||
|
if (!hasPresets || hasPresets.length === 0) return '';
|
||||||
|
return `<div class="indicator-presets">
|
||||||
|
<button class="preset-indicator" title="${hasPresets.length} saved presets">💾</button>
|
||||||
|
</div>`;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="indicator-item active ${isExpanded ? 'expanded' : ''}" data-id="${indicator.id}">
|
||||||
|
<div class="indicator-item-main" onclick="window.toggleIndicatorExpand && window.toggleIndicatorExpand('${indicator.id}');">
|
||||||
|
<div class="drag-handle" title="Drag to reorder">⋮⋮</div>
|
||||||
|
<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}
|
||||||
|
<button class="indicator-btn favorite" onclick="event.stopPropagation(); window.toggleFavorite && window.toggleFavorite('${indicator.type}')" title="Add to favorites">
|
||||||
|
${isFavorite ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
<button class="indicator-btn expand ${isExpanded ? 'rotated' : ''}" title="Show settings">
|
||||||
|
${isExpanded ? '▼' : '▶'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${isExpanded ? `
|
||||||
|
<div class="indicator-config">
|
||||||
|
${typeof renderIndicatorConfig === 'function' ? renderIndicatorConfig(indicator, meta) : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPresetIndicatorIndicator(meta, indicator) {
|
||||||
|
const hasPresets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
|
||||||
|
if (!hasPresets || hasPresets.length === 0) return '';
|
||||||
|
|
||||||
|
return `<button class="preset-indicator" title="${hasPresets.length} saved presets" onclick="event.stopPropagation(); window.showPresets && window.showPresets('${meta.name}')">💾</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorConfig(indicator, meta) {
|
||||||
|
const plotGroups = groupPlotsByColor(meta?.plots || []);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="config-sections">
|
||||||
|
<!-- Colors -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Visual Settings</div>
|
||||||
|
${plotGroups.map(group => {
|
||||||
|
const firstIdx = group.indices[0];
|
||||||
|
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx]?.color || getDefaultColor(activeIndicators.indexOf(indicator));
|
||||||
|
return `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${group.name} Color</label>
|
||||||
|
<div class="color-picker">
|
||||||
|
<input type="color" id="color_${indicator.id}_${firstIdx}" value="${color}" onchange="window.updateIndicatorColor && window.updateIndicatorColor('${indicator.id}', ${firstIdx}, this.value)">
|
||||||
|
<span class="color-preview" style="background: ${color};"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`.trim() + '';
|
||||||
|
}).join('')}
|
||||||
|
|
||||||
|
${indicator.type !== 'rsi' ? `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Line Type</label>
|
||||||
|
<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineType', this.value)">
|
||||||
|
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Line Width</label>
|
||||||
|
<input type="range" min="1" max="5" value="${indicator.params._lineWidth || 2}" onchange="this.nextElementSibling.textContent = this.value; window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '_lineWidth', parseInt(this.value))">
|
||||||
|
<span class="range-value">${indicator.params._lineWidth || 2}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Parameters</div>
|
||||||
|
${meta.inputs.map(input => `
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${input.label}</label>
|
||||||
|
${input.type === 'select' ?
|
||||||
|
`<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
|
||||||
|
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
|
||||||
|
</select>` :
|
||||||
|
`<input
|
||||||
|
type="number"
|
||||||
|
value="${indicator.params[input.name]}"
|
||||||
|
${input.min !== undefined ? `min="${input.min}"` : ''}
|
||||||
|
${input.max !== undefined ? `max="${input.max}"` : ''}
|
||||||
|
${input.step !== undefined ? `step="${input.step}"` : ''}
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
|
||||||
|
>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${meta?.inputs && meta.inputs.length > 0 ? `
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Parameters</div>
|
||||||
|
${meta.inputs.map(input => `
|
||||||
|
${console.log("[DEBUG] Input:", input.name, "value:", indicator.params[input.name])}`
|
||||||
|
<div class="config-row">
|
||||||
|
<label>${input.label}</label>
|
||||||
|
${input.type === 'select' ?
|
||||||
|
`<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', this.value)">
|
||||||
|
${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}
|
||||||
|
</select>` :
|
||||||
|
`<input
|
||||||
|
type="number"
|
||||||
|
value="${indicator.params[input.name]}"
|
||||||
|
${input.min !== undefined ? `min="${input.min}"` : ''}
|
||||||
|
${input.max !== undefined ? `max="${input.max}"` : ''}
|
||||||
|
${input.step !== undefined ? `step="${input.step}"` : ''}
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', '${input.name}', parseFloat(this.value))"
|
||||||
|
>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">
|
||||||
|
Presets
|
||||||
|
<button class="preset-action-btn" onclick="window.savePreset && window.savePreset('${indicator.id}')">+ Save Preset</button>
|
||||||
|
</div>
|
||||||
|
${typeof renderIndicatorPresets === 'function' ? renderIndicatorPresets(indicator, meta) : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-actions">
|
||||||
|
<button class="btn-secondary" onclick="window.resetIndicator && window.resetIndicator('${indicator.id}')">Reset to Defaults</button>
|
||||||
|
<button class="btn-danger" onclick="window.removeIndicator && window.removeIndicator('${indicator.id}')">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorPresets(indicator, meta) {
|
||||||
|
const presets = typeof getPresetsForIndicator === 'function' ? getPresetsForIndicator(meta.name) : [];
|
||||||
|
|
||||||
|
return presets.length > 0 ? `
|
||||||
|
<div class="presets-list">
|
||||||
|
${presets.map(p => {
|
||||||
|
const isApplied = meta.inputs.every(input =>
|
||||||
|
(indicator.params[input.name] === (preset.values?.[input.name] ?? input.default))
|
||||||
|
);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="preset-item ${isApplied ? 'applied' : ''}" data-preset="${preset.id}">
|
||||||
|
<span class="preset-label" onclick="window.applyPreset && window.applyPreset('${indicator.id}', '${preset.id}')">${preset.name}</span>
|
||||||
|
<button class="preset-delete" onclick="window.deletePreset && window.deletePreset('${preset.id}')">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : '<div class="no-presets">No saved presets</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
function setupEventListeners() {
|
||||||
|
const container = document.getElementById('indicatorPanel');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
console.log('[IndicatorPanel] Setting up event listeners...');
|
||||||
|
|
||||||
|
// Single event delegation handler for add button
|
||||||
|
container.addEventListener('click', (e) => {
|
||||||
|
const addBtn = e.target.closest('.indicator-btn.add');
|
||||||
|
if (addBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const type = addBtn.dataset.type;
|
||||||
|
if (type && window.addIndicator) {
|
||||||
|
console.log('[IndicatorPanel] Add button clicked for type:', type);
|
||||||
|
window.addIndicator(type);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand/collapse button
|
||||||
|
const expandBtn = e.target.closest('.indicator-btn.expand');
|
||||||
|
if (expandBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = expandBtn.dataset.id;
|
||||||
|
if (id && window.toggleIndicatorExpand) {
|
||||||
|
window.toggleIndicatorExpand(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove button
|
||||||
|
const removeBtn = e.target.closest('.indicator-btn.remove');
|
||||||
|
if (removeBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = removeBtn.dataset.id;
|
||||||
|
if (id && window.removeIndicatorById) {
|
||||||
|
window.removeIndicatorById(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favorite button
|
||||||
|
const favoriteBtn = e.target.closest('.indicator-btn.favorite');
|
||||||
|
if (favoriteBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const type = favoriteBtn.dataset.type;
|
||||||
|
if (type && window.toggleFavorite) {
|
||||||
|
window.toggleFavorite(type);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search input
|
||||||
|
const searchInput = document.getElementById('indicatorSearch');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
searchQuery = e.target.value;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search clear button
|
||||||
|
const searchClear = container.querySelector('.search-clear');
|
||||||
|
if (searchClear) {
|
||||||
|
searchClear.addEventListener('click', (e) => {
|
||||||
|
searchQuery = '';
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category tabs
|
||||||
|
document.querySelectorAll('.category-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
selectedCategory = tab.dataset.category;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear all button
|
||||||
|
const clearAllBtn = container.querySelector('.clear-all');
|
||||||
|
if (clearAllBtn) {
|
||||||
|
clearAllBtn.addEventListener('click', () => {
|
||||||
|
window.clearAllIndicators();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[IndicatorPanel] Event listeners setup complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
window.toggleIndicatorExpand = function(id) {
|
||||||
|
configuringId = configuringId === id ? null : id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearSearch = function() {
|
||||||
|
searchQuery = '';
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateIndicatorColor = function(id, index, color) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.params[`_color_${index}`] = color;
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateIndicatorSetting = function(id, key, value) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.params[key] = value;
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearAllIndicators = function() {
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
ind.series?.forEach(s => {
|
||||||
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
activeIndicators = [];
|
||||||
|
configuringId = null;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIndicatorById(id) {
|
||||||
|
const idx = activeIndicators.findIndex(a => a.id === id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
activeIndicators[idx].series?.forEach(s => {
|
||||||
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.splice(idx, 1);
|
||||||
|
|
||||||
|
if (configuringId === id) {
|
||||||
|
configuringId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presets
|
||||||
|
function getPresetsForIndicator(indicatorName) {
|
||||||
|
if (!userPresets || !userPresets.presets) return [];
|
||||||
|
return userPresets.presets.filter(p => p.indicatorName === indicatorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.savePreset = function(id) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
const presetName = prompt('Enter preset name:');
|
||||||
|
if (!presetName) return;
|
||||||
|
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
const preset = {
|
||||||
|
id: `preset_${Date.now()}`,
|
||||||
|
name: presetName,
|
||||||
|
indicatorName: meta.name,
|
||||||
|
values: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
meta.inputs.forEach(input => {
|
||||||
|
preset.values[input.name] = indicator.params[input.name];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userPresets.presets) userPresets.presets = [];
|
||||||
|
userPresets.presets.push(preset);
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
|
||||||
|
alert(`Preset "${presetName}" saved!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.applyPreset = function(id, presetId) {
|
||||||
|
const allPresets = (userPresets?.presets || []).filter(p => typeof p === 'object' && p.id);
|
||||||
|
const preset = allPresets.find(p => p.id === presetId);
|
||||||
|
if (!preset) return;
|
||||||
|
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
Object.keys(preset.values).forEach(key => {
|
||||||
|
indicator.params[key] = preset.values[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deletePreset = function(presetId) {
|
||||||
|
if (!confirm('Delete this preset?')) return;
|
||||||
|
|
||||||
|
if (userPresets?.presets) {
|
||||||
|
userPresets.presets = userPresets.presets.filter(p => p.id !== presetId);
|
||||||
|
saveUserPresets();
|
||||||
|
renderIndicatorPanel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.showPresets = function(indicatorName) {
|
||||||
|
const presets = getPresetsForIndicator(indicatorName);
|
||||||
|
if (presets.length === 0) {
|
||||||
|
alert('No saved presets for this indicator');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = window.open('', '_blank', 'width=400,height=500');
|
||||||
|
|
||||||
|
let htmlContent =
|
||||||
|
'<html><head><title>Presets - ' + indicatorName + '</title><style>' +
|
||||||
|
'body { font-family: sans-serif; padding: 20px; background: #1e222d; color: #d1d4dc; }' +
|
||||||
|
'.preset { padding: 10px; margin: 5px; background: #131722; border-radius: 4px; }' +
|
||||||
|
'.preset:hover { background: #2a2e39; cursor: pointer; }' +
|
||||||
|
'</style></head><body>' +
|
||||||
|
'<h3>' + indicatorName + ' Presets</h3>';
|
||||||
|
|
||||||
|
presets.forEach(p => {
|
||||||
|
htmlContent += '<div class="preset" onclick="opener.applyPresetFromWindow(' + "'" + p.id + "'" + ')">' + p.name + '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
htmlContent += '</body></html>';
|
||||||
|
|
||||||
|
menu.document.write(htmlContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.applyPresetFromWindow = function(presetId) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === configuringId);
|
||||||
|
if (!indicator) return;
|
||||||
|
applyPreset(indicator.id, presetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
function addIndicator(type) {
|
||||||
|
const IndicatorClass = IR?.[type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const id = `${type}_${nextInstanceId++}`;
|
||||||
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
|
const metadata = instance.getMetadata();
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
_lineType: 'solid',
|
||||||
|
_lineWidth: 2
|
||||||
|
};
|
||||||
|
metadata.plots.forEach((plot, idx) => {
|
||||||
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
|
});
|
||||||
|
metadata.inputs.forEach(input => {
|
||||||
|
params[input.name] = input.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.push({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name: metadata.name,
|
||||||
|
params,
|
||||||
|
plots: metadata.plots,
|
||||||
|
series: [],
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
// Don't set configuringId so indicators are NOT expanded by default
|
||||||
|
renderIndicatorPanel();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
function saveUserPresets() {
|
||||||
|
localStorage.setItem('indicator_presets', JSON.stringify(userPresets));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
||||||
|
const results = instance.calculate(candles);
|
||||||
|
indicator.series = [];
|
||||||
|
|
||||||
|
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||||
|
const lineWidth = indicator.params._lineWidth || 2;
|
||||||
|
|
||||||
|
const firstNonNull = results?.find(r => r !== null && r !== undefined);
|
||||||
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
|
|
||||||
|
meta.plots.forEach((plot, plotIdx) => {
|
||||||
|
if (isObjectResult) {
|
||||||
|
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||||
|
if (!hasData) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||||
|
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
let value;
|
||||||
|
if (isObjectResult) {
|
||||||
|
value = results[i]?.[plot.id];
|
||||||
|
} else {
|
||||||
|
value = results[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
data.push({
|
||||||
|
time: candles[i].time,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) return;
|
||||||
|
|
||||||
|
let series;
|
||||||
|
let plotLineStyle = lineStyle;
|
||||||
|
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
|
||||||
|
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
|
||||||
|
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
|
||||||
|
|
||||||
|
if (plot.type === 'histogram') {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
|
||||||
|
color: plotColor,
|
||||||
|
priceFormat: { type: 'price', precision: 4, minMove: 0.0001 },
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: false
|
||||||
|
}, paneIndex);
|
||||||
|
} else if (plot.type === 'baseline') {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
|
||||||
|
baseValue: { type: 'price', price: plot.baseValue || 0 },
|
||||||
|
topLineColor: plot.topLineColor || plotColor,
|
||||||
|
topFillColor1: plot.topFillColor1 || plotColor,
|
||||||
|
topFillColor2: '#00000000',
|
||||||
|
bottomFillColor1: '#00000000',
|
||||||
|
bottomColor: plot.bottomColor || '#00000000',
|
||||||
|
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
} else {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: plotColor,
|
||||||
|
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
series.setData(data);
|
||||||
|
indicator.series.push(series);
|
||||||
|
|
||||||
|
// Create horizontal band lines for RSI
|
||||||
|
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) {}
|
||||||
|
}
|
||||||
|
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: ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create oversold band line
|
||||||
|
indicator.bands.push(mainSeries.createPriceLine({
|
||||||
|
price: oversold,
|
||||||
|
color: '#787B86',
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||||
|
axisLabelVisible: false,
|
||||||
|
title: ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart drawing
|
||||||
|
export function drawIndicatorsOnChart() {
|
||||||
|
if (!window.dashboard || !window.dashboard.chart) return;
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
ind.series?.forEach(s => {
|
||||||
|
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||||
|
if (!candles || candles.length === 0) return;
|
||||||
|
|
||||||
|
const lineStyleMap = {
|
||||||
|
'solid': LightweightCharts.LineStyle.Solid,
|
||||||
|
'dotted': LightweightCharts.LineStyle.Dotted,
|
||||||
|
'dashed': LightweightCharts.LineStyle.Dashed
|
||||||
|
};
|
||||||
|
|
||||||
|
indicatorPanes.clear();
|
||||||
|
nextPaneIndex = 1;
|
||||||
|
|
||||||
|
const overlayIndicators = [];
|
||||||
|
const paneIndicators = [];
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
const IndicatorClass = IR?.[ind.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
if (meta.displayMode === 'pane') {
|
||||||
|
paneIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
} else {
|
||||||
|
overlayIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPanes = 1 + paneIndicators.length;
|
||||||
|
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
|
||||||
|
const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0;
|
||||||
|
|
||||||
|
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
||||||
|
|
||||||
|
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paneIndex = nextPaneIndex++;
|
||||||
|
indicatorPanes.set(indicator.id, paneIndex);
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||||
|
|
||||||
|
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||||
|
if (pane) {
|
||||||
|
pane.setHeight(paneHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions for module access
|
||||||
|
export { addIndicator, removeIndicatorById };
|
||||||
|
|
||||||
|
// Legacy compatibility functions
|
||||||
|
window.renderIndicatorList = renderIndicatorPanel;
|
||||||
|
window.toggleIndicator = addIndicator;
|
||||||
|
window.showIndicatorConfig = function(id) {
|
||||||
|
const ind = activeIndicators.find(a => a.id === id);
|
||||||
|
if (ind) configuringId = id;
|
||||||
|
renderIndicatorPanel();
|
||||||
|
};
|
||||||
|
window.applyIndicatorConfig = function() {
|
||||||
|
// No-op - config is applied immediately
|
||||||
|
};
|
||||||
703
js/ui/indicators-panel.js
Normal file
@ -0,0 +1,703 @@
|
|||||||
|
import { getAvailableIndicators, IndicatorRegistry as IR } from '../indicators/index.js';
|
||||||
|
|
||||||
|
let activeIndicators = [];
|
||||||
|
let configuringId = null;
|
||||||
|
let previewingType = null; // type being previewed (not yet added)
|
||||||
|
let nextInstanceId = 1;
|
||||||
|
|
||||||
|
const DEFAULT_COLORS = ['#2962ff', '#26a69a', '#ef5350', '#ff9800', '#9c27b0', '#00bcd4', '#ffeb3b', '#e91e63'];
|
||||||
|
const LINE_TYPES = ['solid', 'dotted', 'dashed'];
|
||||||
|
|
||||||
|
function getDefaultColor(index) {
|
||||||
|
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlotGroupName(plotId) {
|
||||||
|
if (plotId.toLowerCase().includes('fast')) return 'Fast';
|
||||||
|
if (plotId.toLowerCase().includes('slow')) return 'Slow';
|
||||||
|
if (plotId.toLowerCase().includes('upper')) return 'Upper';
|
||||||
|
if (plotId.toLowerCase().includes('lower')) return 'Lower';
|
||||||
|
if (plotId.toLowerCase().includes('middle') || plotId.toLowerCase().includes('basis')) return 'Middle';
|
||||||
|
if (plotId.toLowerCase().includes('signal')) return 'Signal';
|
||||||
|
if (plotId.toLowerCase().includes('histogram')) return 'Histogram';
|
||||||
|
if (plotId.toLowerCase().includes('k')) return '%K';
|
||||||
|
if (plotId.toLowerCase().includes('d')) return '%D';
|
||||||
|
return plotId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupPlotsByColor(plots) {
|
||||||
|
const groups = {};
|
||||||
|
plots.forEach((plot, idx) => {
|
||||||
|
const groupName = getPlotGroupName(plot.id);
|
||||||
|
if (!groups[groupName]) {
|
||||||
|
groups[groupName] = { name: groupName, indices: [], plots: [] };
|
||||||
|
}
|
||||||
|
groups[groupName].indices.push(idx);
|
||||||
|
groups[groupName].plots.push(plot);
|
||||||
|
});
|
||||||
|
return Object.values(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate a short label for an active indicator showing its key params */
|
||||||
|
function getIndicatorLabel(indicator) {
|
||||||
|
const meta = getIndicatorMeta(indicator);
|
||||||
|
if (!meta) return indicator.name;
|
||||||
|
|
||||||
|
const paramParts = meta.inputs.map(input => {
|
||||||
|
const val = indicator.params[input.name];
|
||||||
|
if (val !== undefined && val !== input.default) return val;
|
||||||
|
if (val !== undefined) return val;
|
||||||
|
return null;
|
||||||
|
}).filter(v => v !== null);
|
||||||
|
|
||||||
|
if (paramParts.length > 0) {
|
||||||
|
return `${indicator.name} (${paramParts.join(', ')})`;
|
||||||
|
}
|
||||||
|
return indicator.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndicatorMeta(indicator) {
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return null;
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
return instance.getMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveIndicators() {
|
||||||
|
return activeIndicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveIndicators(indicators) {
|
||||||
|
activeIndicators = indicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the indicator catalog (available indicators) and active list.
|
||||||
|
* Catalog items are added via double-click (multiple instances allowed).
|
||||||
|
*/
|
||||||
|
export function renderIndicatorList() {
|
||||||
|
const container = document.getElementById('indicatorList');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const available = getAvailableIndicators();
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="indicator-catalog">
|
||||||
|
${available.map(ind => `
|
||||||
|
<div class="indicator-catalog-item ${previewingType === ind.type ? 'previewing' : ''}"
|
||||||
|
title="${ind.description || ''}"
|
||||||
|
data-type="${ind.type}">
|
||||||
|
<span class="indicator-catalog-name">${ind.name}</span>
|
||||||
|
<span class="indicator-catalog-add" data-type="${ind.type}">+</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
${activeIndicators.length > 0 ? `
|
||||||
|
<div class="indicator-active-divider">Active</div>
|
||||||
|
<div class="indicator-active-list">
|
||||||
|
${activeIndicators.map(ind => {
|
||||||
|
const isConfiguring = ind.id === configuringId;
|
||||||
|
const plotGroups = groupPlotsByColor(ind.plots || []);
|
||||||
|
const colorDots = plotGroups.map(group => {
|
||||||
|
const firstIdx = group.indices[0];
|
||||||
|
const color = ind.params[`_color_${firstIdx}`] || '#2962ff';
|
||||||
|
return `<span class="indicator-color-dot" style="background: ${color};"></span>`;
|
||||||
|
}).join('');
|
||||||
|
const label = getIndicatorLabel(ind);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="indicator-active-item ${isConfiguring ? 'configuring' : ''}"
|
||||||
|
data-id="${ind.id}">
|
||||||
|
<span class="indicator-active-eye" data-id="${ind.id}"
|
||||||
|
title="${ind.visible !== false ? 'Hide' : 'Show'}">
|
||||||
|
${ind.visible !== false ? '👁' : '👁🗨'}
|
||||||
|
</span>
|
||||||
|
<span class="indicator-active-name" data-id="${ind.id}">${label}</span>
|
||||||
|
${colorDots}
|
||||||
|
<button class="indicator-config-btn ${isConfiguring ? 'active' : ''}"
|
||||||
|
data-id="${ind.id}" title="Configure">⚙</button>
|
||||||
|
<button class="indicator-remove-btn"
|
||||||
|
data-id="${ind.id}" title="Remove">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Bind events via delegation
|
||||||
|
container.querySelectorAll('.indicator-catalog-item').forEach(el => {
|
||||||
|
el.addEventListener('click', () => previewIndicator(el.dataset.type));
|
||||||
|
el.addEventListener('dblclick', () => addIndicator(el.dataset.type));
|
||||||
|
});
|
||||||
|
container.querySelectorAll('.indicator-catalog-add').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
addIndicator(el.dataset.type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
container.querySelectorAll('.indicator-active-name').forEach(el => {
|
||||||
|
el.addEventListener('click', () => selectIndicatorConfig(el.dataset.id));
|
||||||
|
});
|
||||||
|
container.querySelectorAll('.indicator-config-btn').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectIndicatorConfig(el.dataset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
container.querySelectorAll('.indicator-remove-btn').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeIndicatorById(el.dataset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
container.querySelectorAll('.indicator-active-eye').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleVisibility(el.dataset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateConfigPanel();
|
||||||
|
updateChartLegend();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConfigPanel() {
|
||||||
|
const configPanel = document.getElementById('indicatorConfigPanel');
|
||||||
|
const configButtons = document.getElementById('configButtons');
|
||||||
|
if (!configPanel) return;
|
||||||
|
|
||||||
|
configPanel.style.display = 'block';
|
||||||
|
|
||||||
|
// Active indicator config takes priority over preview
|
||||||
|
const indicator = configuringId ? activeIndicators.find(a => a.id === configuringId) : null;
|
||||||
|
|
||||||
|
if (indicator) {
|
||||||
|
renderIndicatorConfig(indicator);
|
||||||
|
if (configButtons) configButtons.style.display = 'flex';
|
||||||
|
} else if (previewingType) {
|
||||||
|
renderPreviewConfig(previewingType);
|
||||||
|
if (configButtons) configButtons.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
const container = document.getElementById('configForm');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '<div style="text-align: center; color: var(--tv-text-secondary); padding: 20px; font-size: 12px;">Click an indicator to preview its settings</div>';
|
||||||
|
}
|
||||||
|
if (configButtons) configButtons.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Single-click: preview config for a catalog indicator type (read-only) */
|
||||||
|
function previewIndicator(type) {
|
||||||
|
configuringId = null;
|
||||||
|
previewingType = previewingType === type ? null : type;
|
||||||
|
renderIndicatorList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render a read-only preview of an indicator's default config */
|
||||||
|
function renderPreviewConfig(type) {
|
||||||
|
const container = document.getElementById('configForm');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const IndicatorClass = IR?.[type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="font-size: 11px; color: var(--tv-blue); margin-bottom: 4px; font-weight: 600;">${meta.name}</div>
|
||||||
|
<div style="font-size: 11px; color: var(--tv-text-secondary); margin-bottom: 10px;">${meta.description || ''}</div>
|
||||||
|
|
||||||
|
${meta.inputs.map(input => `
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
||||||
|
${input.type === 'select' ?
|
||||||
|
`<select class="sim-input" style="font-size: 12px; padding: 6px;" disabled>${input.options.map(o => `<option ${input.default === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
|
||||||
|
`<input type="number" class="sim-input" value="${input.default}" ${input.step !== undefined ? `step="${input.step}"` : ''} style="font-size: 12px; padding: 6px;" disabled>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
|
||||||
|
<div style="font-size: 10px; color: var(--tv-text-secondary); margin-top: 8px; text-align: center;">Double-click to add to chart</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a new instance of an indicator type */
|
||||||
|
export function addIndicator(type) {
|
||||||
|
const IndicatorClass = IR?.[type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
previewingType = null;
|
||||||
|
const id = `${type}_${nextInstanceId++}`;
|
||||||
|
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||||
|
const metadata = instance.getMetadata();
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
_lineType: 'solid',
|
||||||
|
_lineWidth: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set Hurst-specific defaults
|
||||||
|
if (type === 'hurst') {
|
||||||
|
params.timeframe = 'chart';
|
||||||
|
params.markerBuyShape = 'custom';
|
||||||
|
params.markerSellShape = 'custom';
|
||||||
|
params.markerBuyColor = '#9e9e9e';
|
||||||
|
params.markerSellColor = '#9e9e9e';
|
||||||
|
params.markerBuyCustom = '▲';
|
||||||
|
params.markerSellCustom = '▼';
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.plots.forEach((plot, idx) => {
|
||||||
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
|
});
|
||||||
|
metadata.inputs.forEach(input => {
|
||||||
|
params[input.name] = input.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.push({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name: metadata.name,
|
||||||
|
params,
|
||||||
|
plots: metadata.plots,
|
||||||
|
series: [],
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
|
||||||
|
configuringId = id;
|
||||||
|
|
||||||
|
renderIndicatorList();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIndicatorConfig(id) {
|
||||||
|
previewingType = null;
|
||||||
|
if (configuringId === id) {
|
||||||
|
configuringId = null;
|
||||||
|
} else {
|
||||||
|
configuringId = id;
|
||||||
|
}
|
||||||
|
renderIndicatorList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleVisibility(id) {
|
||||||
|
const indicator = activeIndicators.find(a => a.id === id);
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
indicator.visible = indicator.visible === false ? true : false;
|
||||||
|
|
||||||
|
// Show/hide all series for this indicator
|
||||||
|
indicator.series?.forEach(s => {
|
||||||
|
try {
|
||||||
|
s.applyOptions({ visible: indicator.visible });
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicatorList();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderIndicatorConfig(indicator) {
|
||||||
|
const container = document.getElementById('configForm');
|
||||||
|
if (!container || !indicator) return;
|
||||||
|
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) {
|
||||||
|
container.innerHTML = '<div style="color: var(--tv-red);">Error loading indicator</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: indicator.params, name: indicator.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
const plotGroups = groupPlotsByColor(meta.plots);
|
||||||
|
|
||||||
|
const colorInputs = plotGroups.map(group => {
|
||||||
|
const firstIdx = group.indices[0];
|
||||||
|
const color = indicator.params[`_color_${firstIdx}`] || meta.plots[firstIdx].color || '#2962ff';
|
||||||
|
return `
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${group.name} Color</label>
|
||||||
|
<input type="color" id="config__color_${firstIdx}" value="${color}" style="width: 100%; height: 28px; border: 1px solid var(--tv-border); border-radius: 4px; cursor: pointer; background: var(--tv-bg);">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="font-size: 11px; color: var(--tv-blue); margin-bottom: 8px; font-weight: 600;">${getIndicatorLabel(indicator)}</div>
|
||||||
|
|
||||||
|
${colorInputs}
|
||||||
|
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">Line Type</label>
|
||||||
|
<select id="config__lineType" class="sim-input" style="font-size: 12px; padding: 6px;">
|
||||||
|
${LINE_TYPES.map(lt => `<option value="${lt}" ${indicator.params._lineType === lt ? 'selected' : ''}>${lt.charAt(0).toUpperCase() + lt.slice(1)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">Line Width</label>
|
||||||
|
<input type="number" id="config__lineWidth" class="sim-input" value="${indicator.params._lineWidth || 1}" min="1" max="5" style="font-size: 12px; padding: 6px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${meta.inputs.map(input => `
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<label style="font-size: 10px; color: var(--tv-text-secondary); text-transform: uppercase; display: block; margin-bottom: 4px;">${input.label}</label>
|
||||||
|
${input.type === 'select' ?
|
||||||
|
`<select id="config_${input.name}" class="sim-input" style="font-size: 12px; padding: 6px;">${input.options.map(o => `<option value="${o}" ${indicator.params[input.name] === o ? 'selected' : ''}>${o}</option>`).join('')}</select>` :
|
||||||
|
`<input type="number" id="config_${input.name}" class="sim-input" value="${indicator.params[input.name]}" ${input.min !== undefined ? `min="${input.min}"` : ''} ${input.max !== undefined ? `max="${input.max}"` : ''} ${input.step !== undefined ? `step="${input.step}"` : ''} style="font-size: 12px; padding: 6px;">`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyIndicatorConfig() {
|
||||||
|
const indicator = configuringId ? activeIndicators.find(a => a.id === configuringId) : null;
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
const IndicatorClass = IR?.[indicator.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: indicator.type, params: {}, name: indicator.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
const plotGroups = groupPlotsByColor(meta.plots);
|
||||||
|
plotGroups.forEach(group => {
|
||||||
|
const firstIdx = group.indices[0];
|
||||||
|
const colorEl = document.getElementById(`config__color_${firstIdx}`);
|
||||||
|
if (colorEl) {
|
||||||
|
const color = colorEl.value;
|
||||||
|
group.indices.forEach(idx => {
|
||||||
|
indicator.params[`_color_${idx}`] = color;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const lineTypeEl = document.getElementById('config__lineType');
|
||||||
|
const lineWidthEl = document.getElementById('config__lineWidth');
|
||||||
|
|
||||||
|
if (lineTypeEl) indicator.params._lineType = lineTypeEl.value;
|
||||||
|
if (lineWidthEl) indicator.params._lineWidth = parseInt(lineWidthEl.value);
|
||||||
|
|
||||||
|
meta.inputs.forEach(input => {
|
||||||
|
const el = document.getElementById(`config_${input.name}`);
|
||||||
|
if (el) {
|
||||||
|
indicator.params[input.name] = input.type === 'select' ? el.value : parseFloat(el.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicatorList();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeIndicator() {
|
||||||
|
if (!configuringId) return;
|
||||||
|
removeIndicatorById(configuringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeIndicatorById(id) {
|
||||||
|
const idx = activeIndicators.findIndex(a => a.id === id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
activeIndicators[idx].series?.forEach(s => {
|
||||||
|
try { window.dashboard?.chart?.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
activeIndicators.splice(idx, 1);
|
||||||
|
|
||||||
|
if (configuringId === id) {
|
||||||
|
configuringId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorList();
|
||||||
|
drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeIndicatorByIndex(index) {
|
||||||
|
if (index < 0 || index >= activeIndicators.length) return;
|
||||||
|
removeIndicatorById(activeIndicators[index].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let indicatorPanes = new Map();
|
||||||
|
let nextPaneIndex = 1;
|
||||||
|
|
||||||
|
export function drawIndicatorsOnChart() {
|
||||||
|
if (!window.dashboard || !window.dashboard.chart) return;
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
ind.series?.forEach(s => {
|
||||||
|
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
|
||||||
|
if (!candles || candles.length === 0) return;
|
||||||
|
|
||||||
|
const lineStyleMap = { 'solid': LightweightCharts.LineStyle.Solid, 'dotted': LightweightCharts.LineStyle.Dotted, 'dashed': LightweightCharts.LineStyle.Dashed };
|
||||||
|
|
||||||
|
indicatorPanes.clear();
|
||||||
|
nextPaneIndex = 1;
|
||||||
|
|
||||||
|
const overlayIndicators = [];
|
||||||
|
const paneIndicators = [];
|
||||||
|
|
||||||
|
activeIndicators.forEach(ind => {
|
||||||
|
const IndicatorClass = IR?.[ind.type];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
|
||||||
|
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
|
||||||
|
const meta = instance.getMetadata();
|
||||||
|
|
||||||
|
if (meta.displayMode === 'pane') {
|
||||||
|
paneIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
} else {
|
||||||
|
overlayIndicators.push({ indicator: ind, meta, instance });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPanes = 1 + paneIndicators.length;
|
||||||
|
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
|
||||||
|
const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0;
|
||||||
|
|
||||||
|
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
|
||||||
|
|
||||||
|
overlayIndicators.forEach(({ indicator, meta, instance }) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||||
|
if (indicator.visible === false) {
|
||||||
|
indicator.series = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paneIndex = nextPaneIndex++;
|
||||||
|
indicatorPanes.set(indicator.id, paneIndex);
|
||||||
|
|
||||||
|
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||||
|
|
||||||
|
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||||
|
if (pane) {
|
||||||
|
pane.setHeight(paneHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateChartLegend();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
|
||||||
|
let results = instance.calculate(candles);
|
||||||
|
if (!results || !Array.isArray(results)) {
|
||||||
|
console.error(`[renderIndicatorOnPane] ${indicator.name}: Failed to get valid results (got ${typeof results})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
indicator.series = [];
|
||||||
|
|
||||||
|
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
|
||||||
|
const lineWidth = indicator.params._lineWidth || 1;
|
||||||
|
|
||||||
|
const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null;
|
||||||
|
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||||
|
|
||||||
|
meta.plots.forEach((plot, plotIdx) => {
|
||||||
|
if (isObjectResult) {
|
||||||
|
// Find if this specific plot has any non-null data across all results
|
||||||
|
const hasData = results.some(r => r && r[plot.id] !== undefined && r[plot.id] !== null);
|
||||||
|
if (!hasData) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip hidden plots
|
||||||
|
if (plot.visible === false) return;
|
||||||
|
|
||||||
|
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||||
|
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < candles.length; i++) {
|
||||||
|
let value;
|
||||||
|
if (isObjectResult) {
|
||||||
|
value = results[i]?.[plot.id];
|
||||||
|
} else {
|
||||||
|
value = results[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
data.push({
|
||||||
|
time: candles[i].time,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) return;
|
||||||
|
|
||||||
|
let series;
|
||||||
|
|
||||||
|
// Determine line style for this specific plot
|
||||||
|
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: 0,
|
||||||
|
minMove: 1
|
||||||
|
},
|
||||||
|
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: plot.topFillColor2 || '#00000000',
|
||||||
|
bottomFillColor1: plot.bottomFillColor1 || '#00000000',
|
||||||
|
bottomColor: plot.bottomColor || '#00000000',
|
||||||
|
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
} else {
|
||||||
|
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: plotColor,
|
||||||
|
lineWidth: plot.width !== undefined ? plot.width : lineWidth,
|
||||||
|
lineStyle: plotLineStyle,
|
||||||
|
title: plot.title || '',
|
||||||
|
priceLineVisible: false,
|
||||||
|
lastValueVisible: plot.lastValueVisible !== false
|
||||||
|
}, paneIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
series.setData(data);
|
||||||
|
series.plotId = plot.id;
|
||||||
|
|
||||||
|
// Skip hidden plots (visible: false)
|
||||||
|
if (plot.visible === false) {
|
||||||
|
series.applyOptions({ visible: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator.series.push(series);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render gradient zones if available
|
||||||
|
if (meta.gradientZones && indicator.series.length > 0) {
|
||||||
|
// Find the main series to attach zones to
|
||||||
|
let baseSeries = indicator.series[0];
|
||||||
|
|
||||||
|
meta.gradientZones.forEach(zone => {
|
||||||
|
if (zone.from === undefined || zone.to === undefined) return;
|
||||||
|
|
||||||
|
// We use createPriceLine on the series for horizontal bands with custom colors
|
||||||
|
baseSeries.createPriceLine({
|
||||||
|
price: zone.from,
|
||||||
|
color: zone.color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[^)]+\)/, 'rgb($1, $2, $3)'),
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||||
|
axisLabelVisible: false,
|
||||||
|
title: zone.label || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (zone.to !== zone.from) {
|
||||||
|
baseSeries.createPriceLine({
|
||||||
|
price: zone.to,
|
||||||
|
color: zone.color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[^)]+\)/, 'rgb($1, $2, $3)'),
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||||
|
axisLabelVisible: false,
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update the TradingView-style legend overlay on the chart */
|
||||||
|
export function updateChartLegend() {
|
||||||
|
let legend = document.getElementById('chartIndicatorLegend');
|
||||||
|
if (!legend) {
|
||||||
|
const chartWrapper = document.getElementById('chartWrapper');
|
||||||
|
if (!chartWrapper) return;
|
||||||
|
legend = document.createElement('div');
|
||||||
|
legend.id = 'chartIndicatorLegend';
|
||||||
|
legend.className = 'chart-indicator-legend';
|
||||||
|
chartWrapper.appendChild(legend);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeIndicators.length === 0) {
|
||||||
|
legend.innerHTML = '';
|
||||||
|
legend.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend.style.display = 'flex';
|
||||||
|
legend.innerHTML = activeIndicators.map(ind => {
|
||||||
|
const label = getIndicatorLabel(ind);
|
||||||
|
const plotGroups = groupPlotsByColor(ind.plots || []);
|
||||||
|
const firstColor = ind.params['_color_0'] || '#2962ff';
|
||||||
|
const dimmed = ind.visible === false;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="legend-item ${dimmed ? 'legend-dimmed' : ''} ${ind.id === configuringId ? 'legend-selected' : ''}"
|
||||||
|
data-id="${ind.id}">
|
||||||
|
<span class="legend-dot" style="background: ${firstColor};"></span>
|
||||||
|
<span class="legend-label"></span>
|
||||||
|
<span class="legend-close" data-id="${ind.id}" title="Remove">×</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Bind legend events
|
||||||
|
legend.querySelectorAll('.legend-item').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('legend-close')) return;
|
||||||
|
selectIndicatorConfig(el.dataset.id);
|
||||||
|
renderIndicatorList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
legend.querySelectorAll('.legend-close').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeIndicatorById(el.dataset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy compat: toggleIndicator still works for external callers
|
||||||
|
export function toggleIndicator(type) {
|
||||||
|
addIndicator(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showIndicatorConfig(index) {
|
||||||
|
if (index >= 0 && index < activeIndicators.length) {
|
||||||
|
selectIndicatorConfig(activeIndicators[index].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showIndicatorConfigByType(type) {
|
||||||
|
const ind = activeIndicators.find(a => a.type === type);
|
||||||
|
if (ind) {
|
||||||
|
selectIndicatorConfig(ind.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addIndicator = addIndicator;
|
||||||
|
window.toggleIndicator = toggleIndicator;
|
||||||
|
window.showIndicatorConfig = showIndicatorConfig;
|
||||||
|
window.applyIndicatorConfig = applyIndicatorConfig;
|
||||||
|
window.removeIndicator = removeIndicator;
|
||||||
|
window.removeIndicatorById = removeIndicatorById;
|
||||||
|
window.removeIndicatorByIndex = removeIndicatorByIndex;
|
||||||
|
window.drawIndicatorsOnChart = drawIndicatorsOnChart;
|
||||||
117
js/ui/markers-plugin.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
export class SeriesMarkersPrimitive {
|
||||||
|
constructor(markers) {
|
||||||
|
this._markers = markers || [];
|
||||||
|
this._paneViews = [new MarkersPaneView(this)];
|
||||||
|
}
|
||||||
|
|
||||||
|
setMarkers(markers) {
|
||||||
|
this._markers = markers;
|
||||||
|
if (this._requestUpdate) {
|
||||||
|
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() {}
|
||||||
|
|
||||||
|
paneViews() {
|
||||||
|
return this._paneViews;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkersPaneView {
|
||||||
|
constructor(source) {
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer() {
|
||||||
|
return new MarkersRenderer(this._source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkersRenderer {
|
||||||
|
constructor(source) {
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(target) {
|
||||||
|
if (!this._source._chart || !this._source._series) return;
|
||||||
|
|
||||||
|
// Lightweight Charts v5 wraps context
|
||||||
|
const ctx = target.context;
|
||||||
|
const series = this._source._series;
|
||||||
|
const chart = this._source._chart;
|
||||||
|
const markers = this._source._markers;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// Ensure markers are sorted by time (usually already done)
|
||||||
|
for (const marker of markers) {
|
||||||
|
const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time);
|
||||||
|
if (timeCoordinate === null) continue;
|
||||||
|
|
||||||
|
// To position above or below bar, we need the candle data or we use the marker.value if provided
|
||||||
|
// For true aboveBar/belowBar without candle data, we might just use series.priceToCoordinate on marker.value
|
||||||
|
let price = marker.value;
|
||||||
|
// Fallbacks if no value provided (which our calculator does provide)
|
||||||
|
if (!price) continue;
|
||||||
|
|
||||||
|
const priceCoordinate = series.priceToCoordinate(price);
|
||||||
|
if (priceCoordinate === null) continue;
|
||||||
|
|
||||||
|
const x = timeCoordinate;
|
||||||
|
const size = 5;
|
||||||
|
const margin = 12; // Gap between price and marker
|
||||||
|
const isAbove = marker.position === 'aboveBar';
|
||||||
|
const y = isAbove ? priceCoordinate - margin : priceCoordinate + margin;
|
||||||
|
|
||||||
|
ctx.fillStyle = marker.color || '#26a69a';
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
if (marker.shape === 'arrowUp' || (!marker.shape && !isAbove)) {
|
||||||
|
ctx.moveTo(x, y - size);
|
||||||
|
ctx.lineTo(x - size, y + size);
|
||||||
|
ctx.lineTo(x + size, y + size);
|
||||||
|
} else if (marker.shape === 'arrowDown' || (!marker.shape && isAbove)) {
|
||||||
|
ctx.moveTo(x, y + size);
|
||||||
|
ctx.lineTo(x - size, y - size);
|
||||||
|
ctx.lineTo(x + size, y - size);
|
||||||
|
} else if (marker.shape === 'circle') {
|
||||||
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||||||
|
} else if (marker.shape === 'square') {
|
||||||
|
ctx.rect(x - size, y - size, size * 2, size * 2);
|
||||||
|
} else if (marker.shape === 'custom' && marker.text) {
|
||||||
|
ctx.font = '12px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(marker.text, x, y);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Default triangle
|
||||||
|
if (isAbove) {
|
||||||
|
ctx.moveTo(x, y + size);
|
||||||
|
ctx.lineTo(x - size, y - size);
|
||||||
|
ctx.lineTo(x + size, y - size);
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(x, y - size);
|
||||||
|
ctx.lineTo(x - size, y + size);
|
||||||
|
ctx.lineTo(x + size, y + size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
73
js/ui/sidebar.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
export function toggleSidebar() {
|
||||||
|
const sidebar = document.getElementById('rightSidebar');
|
||||||
|
sidebar.classList.toggle('collapsed');
|
||||||
|
localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
|
||||||
|
|
||||||
|
// Resize chart after sidebar toggle
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.dashboard && window.dashboard.chart) {
|
||||||
|
const container = document.getElementById('chart');
|
||||||
|
window.dashboard.chart.applyOptions({
|
||||||
|
width: container.clientWidth,
|
||||||
|
height: container.clientHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 350); // Wait for CSS transition
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreSidebarState() {
|
||||||
|
const collapsed = localStorage.getItem('sidebar_collapsed') !== 'false'; // Default to collapsed
|
||||||
|
const sidebar = document.getElementById('rightSidebar');
|
||||||
|
if (collapsed && sidebar) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Management
|
||||||
|
let activeTab = 'indicators';
|
||||||
|
|
||||||
|
export function initSidebarTabs() {
|
||||||
|
const tabs = document.querySelectorAll('.sidebar-tab');
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
switchTab(tab.dataset.tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function switchTab(tabId) {
|
||||||
|
activeTab = tabId;
|
||||||
|
localStorage.setItem('sidebar_active_tab', tabId);
|
||||||
|
|
||||||
|
document.querySelectorAll('.sidebar-tab').forEach(tab => {
|
||||||
|
tab.classList.toggle('active', tab.dataset.tab === tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.sidebar-tab-panel').forEach(panel => {
|
||||||
|
panel.classList.toggle('active', panel.id === `tab-${tabId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tabId === 'indicators') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.drawIndicatorsOnChart) {
|
||||||
|
window.drawIndicatorsOnChart();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
} else if (tabId === 'strategy') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.renderStrategyPanel) {
|
||||||
|
window.renderStrategyPanel();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveTab() {
|
||||||
|
return activeTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreSidebarTabState() {
|
||||||
|
const savedTab = localStorage.getItem('sidebar_active_tab') || 'indicators';
|
||||||
|
switchTab(savedTab);
|
||||||
|
}
|
||||||
231
js/ui/signal-markers.js
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import { IndicatorRegistry } from '../indicators/index.js';
|
||||||
|
|
||||||
|
export function calculateSignalMarkers(candles) {
|
||||||
|
const activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
const markers = [];
|
||||||
|
|
||||||
|
if (!candles || candles.length < 2) {
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const indicator of activeIndicators) {
|
||||||
|
if (indicator.params.showMarkers === false || indicator.params.showMarkers === 'false') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[SignalMarkers] Processing indicator:', indicator.type, 'showMarkers:', indicator.params.showMarkers);
|
||||||
|
|
||||||
|
// Use cache if available
|
||||||
|
let results = indicator.cachedResults;
|
||||||
|
if (!results || !Array.isArray(results) || results.length !== candles.length) {
|
||||||
|
const IndicatorClass = IndicatorRegistry[indicator.type];
|
||||||
|
if (!IndicatorClass) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const instance = new IndicatorClass(indicator);
|
||||||
|
results = instance.calculate(candles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results || !Array.isArray(results) || results.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicatorMarkers = findCrossoverMarkers(indicator, candles, results);
|
||||||
|
markers.push(...indicatorMarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
markers.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCrossoverMarkers(indicator, candles, results) {
|
||||||
|
const markers = [];
|
||||||
|
const overbought = indicator.params?.overbought || 70;
|
||||||
|
const oversold = indicator.params?.oversold || 30;
|
||||||
|
const indicatorType = indicator.type;
|
||||||
|
|
||||||
|
const buyColor = indicator.params?.markerBuyColor || '#26a69a';
|
||||||
|
const sellColor = indicator.params?.markerSellColor || '#ef5350';
|
||||||
|
const buyShape = indicator.params?.markerBuyShape || 'arrowUp';
|
||||||
|
const sellShape = indicator.params?.markerSellShape || 'arrowDown';
|
||||||
|
const buyCustom = indicator.params?.markerBuyCustom || '◭';
|
||||||
|
const sellCustom = indicator.params?.markerSellCustom || '▼';
|
||||||
|
|
||||||
|
for (let i = 1; i < results.length; i++) {
|
||||||
|
const candle = candles[i];
|
||||||
|
const prevCandle = candles[i - 1];
|
||||||
|
const result = results[i];
|
||||||
|
const prevResult = results[i - 1];
|
||||||
|
|
||||||
|
if (!result || !prevResult) continue;
|
||||||
|
|
||||||
|
if (indicatorType === 'rsi' || indicatorType === 'stoch') {
|
||||||
|
const rsi = result.rsi ?? result;
|
||||||
|
const prevRsi = prevResult.rsi ?? prevResult;
|
||||||
|
|
||||||
|
if (rsi === undefined || prevRsi === undefined) continue;
|
||||||
|
|
||||||
|
if (prevRsi > overbought && rsi <= overbought) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevRsi < oversold && rsi >= oversold) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'macd') {
|
||||||
|
const macd = result.macd ?? result.MACD;
|
||||||
|
const signal = result.signal ?? result.signalLine;
|
||||||
|
const prevMacd = prevResult.macd ?? prevResult.MACD;
|
||||||
|
const prevSignal = prevResult.signal ?? prevResult.signalLine;
|
||||||
|
|
||||||
|
if (macd === undefined || signal === undefined || prevMacd === undefined || prevSignal === undefined) continue;
|
||||||
|
|
||||||
|
const macdAbovePrev = prevMacd > prevSignal;
|
||||||
|
const macdAboveNow = macd > signal;
|
||||||
|
|
||||||
|
if (macdAbovePrev && !macdAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!macdAbovePrev && macdAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'bb') {
|
||||||
|
const upper = result.upper ?? result.upperBand;
|
||||||
|
const lower = result.lower ?? result.lowerBand;
|
||||||
|
|
||||||
|
if (upper === undefined || lower === undefined) continue;
|
||||||
|
|
||||||
|
const priceAboveUpperPrev = prevCandle.close > (prevResult.upper ?? prevResult.upperBand);
|
||||||
|
const priceAboveUpperNow = candle.close > upper;
|
||||||
|
|
||||||
|
if (priceAboveUpperPrev && !priceAboveUpperNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceAboveUpperPrev && priceAboveUpperNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceBelowLowerPrev = prevCandle.close < (prevResult.lower ?? prevResult.lowerBand);
|
||||||
|
const priceBelowLowerNow = candle.close < lower;
|
||||||
|
|
||||||
|
if (priceBelowLowerPrev && !priceBelowLowerNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceBelowLowerPrev && priceBelowLowerNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'hurst') {
|
||||||
|
const upper = result.upper;
|
||||||
|
const lower = result.lower;
|
||||||
|
const prevUpper = prevResult?.upper;
|
||||||
|
const prevLower = prevResult?.lower;
|
||||||
|
|
||||||
|
if (upper === undefined || lower === undefined ||
|
||||||
|
prevUpper === undefined || prevLower === undefined) continue;
|
||||||
|
|
||||||
|
// BUY: price crosses down below lower band (was above, now below)
|
||||||
|
if (prevCandle.close > prevLower && candle.close < lower) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELL: price crosses down below upper band (was above, now below)
|
||||||
|
if (prevCandle.close > prevUpper && candle.close < upper) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const ma = result.ma ?? result;
|
||||||
|
const prevMa = prevResult.ma ?? prevResult;
|
||||||
|
|
||||||
|
if (ma === undefined || prevMa === undefined) continue;
|
||||||
|
|
||||||
|
const priceAbovePrev = prevCandle.close > prevMa;
|
||||||
|
const priceAboveNow = candle.close > ma;
|
||||||
|
|
||||||
|
if (priceAbovePrev && !priceAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: sellColor,
|
||||||
|
shape: sellShape === 'custom' ? '' : sellShape,
|
||||||
|
text: sellShape === 'custom' ? sellCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceAbovePrev && priceAboveNow) {
|
||||||
|
markers.push({
|
||||||
|
time: candle.time,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: buyColor,
|
||||||
|
shape: buyShape === 'custom' ? '' : buyShape,
|
||||||
|
text: buyShape === 'custom' ? buyCustom : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
367
js/ui/signals-calculator.js
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
// Signal Calculator - orchestrates signal calculation using indicator-specific functions
|
||||||
|
// Signal calculation logic is now in each indicator file
|
||||||
|
|
||||||
|
import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate signal for an indicator
|
||||||
|
* @param {Object} indicator - Indicator configuration
|
||||||
|
* @param {Array} candles - Candle data array
|
||||||
|
* @param {Object} indicatorValues - Computed indicator values for last candle
|
||||||
|
* @param {Object} prevIndicatorValues - Computed indicator values for previous candle
|
||||||
|
* @returns {Object} Signal object with type, strength, value, reasoning
|
||||||
|
*/
|
||||||
|
function calculateIndicatorSignal(indicator, candles, indicatorValues, prevIndicatorValues) {
|
||||||
|
const signalFunction = getSignalFunction(indicator.type);
|
||||||
|
|
||||||
|
if (!signalFunction) {
|
||||||
|
console.warn('[Signals] No signal function for indicator type:', indicator.type);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCandle = candles[candles.length - 1];
|
||||||
|
const prevCandle = candles[candles.length - 2];
|
||||||
|
|
||||||
|
return signalFunction(indicator, lastCandle, prevCandle, indicatorValues, prevIndicatorValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate aggregate summary signal from all indicators
|
||||||
|
*/
|
||||||
|
export function calculateSummarySignal(signals) {
|
||||||
|
console.log('[calculateSummarySignal] Input signals:', signals?.length);
|
||||||
|
|
||||||
|
if (!signals || signals.length === 0) {
|
||||||
|
return {
|
||||||
|
signal: 'hold',
|
||||||
|
strength: 0,
|
||||||
|
reasoning: 'No active indicators',
|
||||||
|
buyCount: 0,
|
||||||
|
sellCount: 0,
|
||||||
|
holdCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const buySignals = signals.filter(s => s.signal === 'buy');
|
||||||
|
const sellSignals = signals.filter(s => s.signal === 'sell');
|
||||||
|
const holdSignals = signals.filter(s => s.signal === 'hold');
|
||||||
|
|
||||||
|
const buyCount = buySignals.length;
|
||||||
|
const sellCount = sellSignals.length;
|
||||||
|
const holdCount = holdSignals.length;
|
||||||
|
const total = signals.length;
|
||||||
|
|
||||||
|
console.log('[calculateSummarySignal] BUY:', buyCount, 'SELL:', sellCount, 'HOLD:', holdCount);
|
||||||
|
|
||||||
|
const buyWeight = buySignals.reduce((sum, s) => sum + (s.strength || 0), 0);
|
||||||
|
const sellWeight = sellSignals.reduce((sum, s) => sum + (s.strength || 0), 0);
|
||||||
|
|
||||||
|
let summarySignal, strength, reasoning;
|
||||||
|
|
||||||
|
if (buyCount > sellCount && buyCount > holdCount) {
|
||||||
|
summarySignal = 'buy';
|
||||||
|
const avgBuyStrength = buyWeight / buyCount;
|
||||||
|
strength = Math.round(avgBuyStrength * (buyCount / total));
|
||||||
|
reasoning = `${buyCount} buy signals, ${sellCount} sell, ${holdCount} hold`;
|
||||||
|
} else if (sellCount > buyCount && sellCount > holdCount) {
|
||||||
|
summarySignal = 'sell';
|
||||||
|
const avgSellStrength = sellWeight / sellCount;
|
||||||
|
strength = Math.round(avgSellStrength * (sellCount / total));
|
||||||
|
reasoning = `${sellCount} sell signals, ${buyCount} buy, ${holdCount} hold`;
|
||||||
|
} else {
|
||||||
|
summarySignal = 'hold';
|
||||||
|
strength = 30;
|
||||||
|
reasoning = `Mixed signals: ${buyCount} buy, ${sellCount} sell, ${holdCount} hold`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
signal: summarySignal,
|
||||||
|
strength: Math.min(Math.max(strength, 0), 100),
|
||||||
|
reasoning,
|
||||||
|
buyCount,
|
||||||
|
sellCount,
|
||||||
|
holdCount,
|
||||||
|
color: summarySignal === 'buy' ? '#26a69a' : summarySignal === 'sell' ? '#ef5350' : '#787b86'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[calculateSummarySignal] Result:', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate historical crossovers for all indicators based on full candle history
|
||||||
|
* Finds the last time each indicator crossed from BUY to SELL or SELL to BUY
|
||||||
|
*/
|
||||||
|
function calculateHistoricalCrossovers(activeIndicators, candles) {
|
||||||
|
activeIndicators.forEach(indicator => {
|
||||||
|
const indicatorType = indicator.type || indicator.indicatorType;
|
||||||
|
|
||||||
|
// Recalculate indicator values for all candles (use cache if valid)
|
||||||
|
let results = indicator.cachedResults;
|
||||||
|
if (!results || !Array.isArray(results) || results.length !== candles.length) {
|
||||||
|
const IndicatorClass = IndicatorRegistry[indicatorType];
|
||||||
|
if (!IndicatorClass) return;
|
||||||
|
const instance = new IndicatorClass(indicator);
|
||||||
|
results = instance.calculate(candles);
|
||||||
|
// Don't save back to cache here, let drawIndicatorsOnChart be the source of truth for cache
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results || !Array.isArray(results) || results.length === 0) return;
|
||||||
|
|
||||||
|
// Find the most recent crossover by going backwards from the newest candle
|
||||||
|
// candles are sorted oldest first, newest last
|
||||||
|
let lastCrossoverTimestamp = null;
|
||||||
|
let lastSignalType = null;
|
||||||
|
|
||||||
|
// Get indicator-specific parameters
|
||||||
|
const overbought = indicator.params?.overbought || 70;
|
||||||
|
const oversold = indicator.params?.oversold || 30;
|
||||||
|
|
||||||
|
for (let i = candles.length - 1; i > 0; i--) {
|
||||||
|
const candle = candles[i]; // newer candle
|
||||||
|
const prevCandle = candles[i-1]; // older candle
|
||||||
|
|
||||||
|
const result = results[i];
|
||||||
|
const prevResult = results[i-1];
|
||||||
|
|
||||||
|
if (!result || !prevResult) continue;
|
||||||
|
|
||||||
|
// Handle different indicator types
|
||||||
|
if (indicatorType === 'rsi' || indicatorType === 'stoch') {
|
||||||
|
// RSI/Stochastic: check crossing overbought/oversold levels
|
||||||
|
const rsi = result.rsi !== undefined ? result.rsi : result;
|
||||||
|
const prevRsi = prevResult.rsi !== undefined ? prevResult.rsi : prevResult;
|
||||||
|
|
||||||
|
if (rsi === undefined || prevRsi === undefined) continue;
|
||||||
|
|
||||||
|
// SELL: crossed down through overbought (was above, now below)
|
||||||
|
if (prevRsi > overbought && rsi <= overbought) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'sell';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// BUY: crossed up through oversold (was below, now above)
|
||||||
|
if (prevRsi < oversold && rsi >= oversold) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'buy';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'hurst') {
|
||||||
|
// Hurst Bands: check price crossing bands
|
||||||
|
const upper = result.upper;
|
||||||
|
const lower = result.lower;
|
||||||
|
const prevUpper = prevResult.upper;
|
||||||
|
const prevLower = prevResult.lower;
|
||||||
|
|
||||||
|
if (upper === undefined || lower === undefined ||
|
||||||
|
prevUpper === undefined || prevLower === undefined) continue;
|
||||||
|
|
||||||
|
// BUY: price crossed down below lower band
|
||||||
|
if (prevCandle.close > prevLower && candle.close < lower) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'buy';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// SELL: price crossed down below upper band
|
||||||
|
if (prevCandle.close > prevUpper && candle.close < upper) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'sell';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MA-style: check price crossing MA
|
||||||
|
const ma = result.ma !== undefined ? result.ma : result;
|
||||||
|
const prevMa = prevResult.ma !== undefined ? prevResult.ma : prevResult;
|
||||||
|
|
||||||
|
if (ma === undefined || prevMa === undefined) continue;
|
||||||
|
|
||||||
|
// Check crossover: price was on one side of MA, now on the other side
|
||||||
|
const priceAbovePrev = prevCandle.close > prevMa;
|
||||||
|
const priceAboveNow = candle.close > ma;
|
||||||
|
|
||||||
|
// SELL signal: price crossed from above to below MA
|
||||||
|
if (priceAbovePrev && !priceAboveNow) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'sell';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// BUY signal: price crossed from below to above MA
|
||||||
|
if (!priceAbovePrev && priceAboveNow) {
|
||||||
|
lastCrossoverTimestamp = candle.time;
|
||||||
|
lastSignalType = 'buy';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update the timestamp based on current data
|
||||||
|
// If crossover found use that time, otherwise use last candle time
|
||||||
|
if (lastCrossoverTimestamp) {
|
||||||
|
console.log(`[HistoricalCross] ${indicatorType}: Found ${lastSignalType} crossover at ${new Date(lastCrossoverTimestamp * 1000).toLocaleString()}`);
|
||||||
|
indicator.lastSignalTimestamp = lastCrossoverTimestamp;
|
||||||
|
indicator.lastSignalType = lastSignalType;
|
||||||
|
} else {
|
||||||
|
// No crossover found - use last candle time
|
||||||
|
const lastCandleTime = candles[candles.length - 1]?.time;
|
||||||
|
if (lastCandleTime) {
|
||||||
|
const lastResult = results[results.length - 1];
|
||||||
|
|
||||||
|
if (indicatorType === 'rsi' || indicatorType === 'stoch') {
|
||||||
|
// RSI/Stochastic: use RSI level to determine signal
|
||||||
|
const rsi = lastResult?.rsi !== undefined ? lastResult.rsi : lastResult;
|
||||||
|
if (rsi !== undefined) {
|
||||||
|
indicator.lastSignalType = rsi > overbought ? 'sell' : (rsi < oversold ? 'buy' : null);
|
||||||
|
indicator.lastSignalTimestamp = lastCandleTime;
|
||||||
|
}
|
||||||
|
} else if (indicatorType === 'hurst') {
|
||||||
|
// Hurst Bands: use price vs bands
|
||||||
|
const upper = lastResult?.upper;
|
||||||
|
const lower = lastResult?.lower;
|
||||||
|
const currentPrice = candles[candles.length - 1]?.close;
|
||||||
|
if (upper !== undefined && lower !== undefined && currentPrice !== undefined) {
|
||||||
|
if (currentPrice < lower) {
|
||||||
|
indicator.lastSignalType = 'buy';
|
||||||
|
} else if (currentPrice > upper) {
|
||||||
|
indicator.lastSignalType = 'sell';
|
||||||
|
} else {
|
||||||
|
indicator.lastSignalType = null;
|
||||||
|
}
|
||||||
|
indicator.lastSignalTimestamp = lastCandleTime;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// MA-style: use price vs MA
|
||||||
|
const ma = lastResult?.ma !== undefined ? lastResult.ma : lastResult;
|
||||||
|
if (ma !== undefined) {
|
||||||
|
const isAbove = candles[candles.length - 1].close > ma;
|
||||||
|
indicator.lastSignalType = isAbove ? 'buy' : 'sell';
|
||||||
|
indicator.lastSignalTimestamp = lastCandleTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate signals for all active indicators
|
||||||
|
* @returns {Array} Array of indicator signals
|
||||||
|
*/
|
||||||
|
export function calculateAllIndicatorSignals() {
|
||||||
|
const activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
const candles = window.dashboard?.allData?.get(window.dashboard?.currentInterval);
|
||||||
|
|
||||||
|
//console.log('[Signals] ========== calculateAllIndicatorSignals START ==========');
|
||||||
|
console.log('[Signals] Active indicators:', activeIndicators.length, 'Candles:', candles?.length || 0);
|
||||||
|
|
||||||
|
if (!candles || candles.length < 2) {
|
||||||
|
//console.log('[Signals] Insufficient candles available:', candles?.length || 0);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeIndicators || activeIndicators.length === 0) {
|
||||||
|
//console.log('[Signals] No active indicators');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const signals = [];
|
||||||
|
|
||||||
|
// Calculate crossovers for all indicators based on historical data
|
||||||
|
calculateHistoricalCrossovers(activeIndicators, candles);
|
||||||
|
|
||||||
|
for (const indicator of activeIndicators) {
|
||||||
|
const IndicatorClass = IndicatorRegistry[indicator.type];
|
||||||
|
if (!IndicatorClass) {
|
||||||
|
console.log('[Signals] No class for indicator type:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cached results if available, otherwise calculate
|
||||||
|
let results = indicator.cachedResults;
|
||||||
|
let meta = indicator.cachedMeta;
|
||||||
|
|
||||||
|
if (!results || !meta || !Array.isArray(results) || results.length !== candles.length) {
|
||||||
|
const instance = new IndicatorClass(indicator);
|
||||||
|
meta = instance.getMetadata();
|
||||||
|
results = instance.calculate(candles);
|
||||||
|
indicator.cachedResults = results;
|
||||||
|
indicator.cachedMeta = meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results || !Array.isArray(results) || results.length === 0) {
|
||||||
|
console.log('[Signals] No valid results for indicator:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastResult = results[results.length - 1];
|
||||||
|
const prevResult = results[results.length - 2];
|
||||||
|
if (lastResult === null || lastResult === undefined) {
|
||||||
|
console.log('[Signals] No valid last result for indicator:', indicator.type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let values;
|
||||||
|
let prevValues;
|
||||||
|
if (typeof lastResult === 'object' && lastResult !== null && !Array.isArray(lastResult)) {
|
||||||
|
values = lastResult;
|
||||||
|
prevValues = prevResult;
|
||||||
|
} else if (typeof lastResult === 'number') {
|
||||||
|
values = { ma: lastResult };
|
||||||
|
prevValues = prevResult ? { ma: prevResult } : undefined;
|
||||||
|
} else {
|
||||||
|
console.log('[Signals] Unexpected result type for', indicator.type, ':', typeof lastResult);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signal = calculateIndicatorSignal(indicator, candles, values, prevValues);
|
||||||
|
|
||||||
|
let currentSignal = signal;
|
||||||
|
let lastSignalDate = indicator.lastSignalTimestamp || null;
|
||||||
|
let lastSignalType = indicator.lastSignalType || null;
|
||||||
|
|
||||||
|
if (!currentSignal || !currentSignal.type) {
|
||||||
|
console.log('[Signals] No valid signal for', indicator.type, '- Using last signal if available');
|
||||||
|
|
||||||
|
if (lastSignalType && lastSignalDate) {
|
||||||
|
currentSignal = {
|
||||||
|
type: lastSignalType,
|
||||||
|
strength: 50,
|
||||||
|
value: candles[candles.length - 1]?.close,
|
||||||
|
reasoning: `No crossover (price equals MA)`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log('[Signals] No previous signal available - Skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentCandleTimestamp = candles[candles.length - 1].time;
|
||||||
|
|
||||||
|
if (currentSignal.type !== lastSignalType || !lastSignalType) {
|
||||||
|
console.log('[Signals] Signal changed for', indicator.type, ':', lastSignalType, '->', currentSignal.type);
|
||||||
|
lastSignalDate = indicator.lastSignalTimestamp || currentCandleTimestamp;
|
||||||
|
lastSignalType = currentSignal.type;
|
||||||
|
indicator.lastSignalTimestamp = lastSignalDate;
|
||||||
|
indicator.lastSignalType = lastSignalType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signals.push({
|
||||||
|
id: indicator.id,
|
||||||
|
name: meta?.name || indicator.type,
|
||||||
|
label: indicator.type?.toUpperCase(),
|
||||||
|
params: meta?.inputs && meta.inputs.length > 0
|
||||||
|
? indicator.params[meta.inputs[0].name]
|
||||||
|
: null,
|
||||||
|
type: indicator.type,
|
||||||
|
signal: currentSignal.type,
|
||||||
|
strength: Math.round(currentSignal.strength),
|
||||||
|
value: currentSignal.value,
|
||||||
|
reasoning: currentSignal.reasoning,
|
||||||
|
color: currentSignal.type === 'buy' ? '#26a69a' : currentSignal.type === 'sell' ? '#ef5350' : '#787b86',
|
||||||
|
lastSignalDate: lastSignalDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('[Signals] ========== calculateAllIndicatorSignals END ==========');
|
||||||
|
console.log('[Signals] Total signals calculated:', signals.length);
|
||||||
|
return signals;
|
||||||
|
}
|
||||||
370
js/ui/strategy-panel.js
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
import { getStrategy, registerStrategy } from '../strategies/index.js';
|
||||||
|
import { PingPongStrategy } from '../strategies/ping-pong.js';
|
||||||
|
|
||||||
|
// Register available strategies
|
||||||
|
registerStrategy('ping_pong', PingPongStrategy);
|
||||||
|
|
||||||
|
let activeIndicators = [];
|
||||||
|
|
||||||
|
function formatDisplayDate(timestamp) {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initStrategyPanel() {
|
||||||
|
window.renderStrategyPanel = renderStrategyPanel;
|
||||||
|
renderStrategyPanel();
|
||||||
|
|
||||||
|
// Listen for indicator changes to update the signal selection list
|
||||||
|
const originalAddIndicator = window.addIndicator;
|
||||||
|
window.addIndicator = function(...args) {
|
||||||
|
const res = originalAddIndicator.apply(this, args);
|
||||||
|
setTimeout(renderStrategyPanel, 100);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalRemoveIndicator = window.removeIndicatorById;
|
||||||
|
window.removeIndicatorById = function(...args) {
|
||||||
|
const res = originalRemoveIndicator.apply(this, args);
|
||||||
|
setTimeout(renderStrategyPanel, 100);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderStrategyPanel() {
|
||||||
|
const container = document.getElementById('strategyPanel');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
|
||||||
|
// For now, we only have Ping-Pong. Later we can add a strategy selector.
|
||||||
|
const currentStrategyId = 'ping_pong';
|
||||||
|
const strategy = getStrategy(currentStrategyId);
|
||||||
|
|
||||||
|
if (!strategy) {
|
||||||
|
container.innerHTML = `<div class="sidebar-section">Strategy not found.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-header">
|
||||||
|
<span>⚙️</span> ${strategy.name} Strategy
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section-content">
|
||||||
|
${strategy.renderUI(activeIndicators, formatDisplayDate)}
|
||||||
|
<button class="sim-run-btn" id="runSimulationBtn">Run Simulation</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="simulationResults" class="sim-results" style="display: none;">
|
||||||
|
<!-- Results will be injected here -->
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Attach strategy specific listeners (like disabling dropdowns when auto-detect is on)
|
||||||
|
if (strategy.attachListeners) {
|
||||||
|
strategy.attachListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('runSimulationBtn').addEventListener('click', () => {
|
||||||
|
strategy.runSimulation(activeIndicators, displayResults);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the display logic here so all strategies can use the same rendering for results
|
||||||
|
let equitySeries = null;
|
||||||
|
let equityChart = null;
|
||||||
|
let posSeries = null;
|
||||||
|
let posSizeChart = null;
|
||||||
|
let tradeMarkers = [];
|
||||||
|
|
||||||
|
function displayResults(trades, equityData, config, endPrice, avgPriceData, posSizeData) {
|
||||||
|
const resultsDiv = document.getElementById('simulationResults');
|
||||||
|
resultsDiv.style.display = 'block';
|
||||||
|
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.setAvgPriceData(avgPriceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryTrades = trades.filter(t => t.recordType === 'entry').length;
|
||||||
|
const exitTrades = trades.filter(t => t.recordType === 'exit').length;
|
||||||
|
const profitableTrades = trades.filter(t => t.recordType === 'exit' && t.pnl > 0).length;
|
||||||
|
const winRate = exitTrades > 0 ? (profitableTrades / exitTrades * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
const startPrice = equityData.usd[0].value / equityData.btc[0].value;
|
||||||
|
const startBtc = config.capital / startPrice;
|
||||||
|
|
||||||
|
const finalUsd = equityData.usd[equityData.usd.length - 1].value;
|
||||||
|
const finalBtc = finalUsd / endPrice;
|
||||||
|
|
||||||
|
const totalPnlUsd = finalUsd - config.capital;
|
||||||
|
const roi = (totalPnlUsd / config.capital * 100).toFixed(2);
|
||||||
|
|
||||||
|
const roiBtc = ((finalBtc - startBtc) / startBtc * 100).toFixed(2);
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = `
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-header">Results</div>
|
||||||
|
<div class="sidebar-section-content">
|
||||||
|
<div class="results-summary">
|
||||||
|
<div class="result-stat">
|
||||||
|
<div class="result-stat-value ${totalPnlUsd >= 0 ? 'positive' : 'negative'}">${roi}%</div>
|
||||||
|
<div class="result-stat-label">ROI (USD)</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-stat">
|
||||||
|
<div class="result-stat-value ${parseFloat(roiBtc) >= 0 ? 'positive' : 'negative'}">${roiBtc}%</div>
|
||||||
|
<div class="result-stat-label">ROI (BTC)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-stat-row">
|
||||||
|
<span>Starting Balance</span>
|
||||||
|
<span class="sim-value">$${config.capital.toFixed(0)} / ${startBtc.toFixed(4)} BTC</span>
|
||||||
|
</div>
|
||||||
|
<div class="sim-stat-row">
|
||||||
|
<span>Final Balance</span>
|
||||||
|
<span class="sim-value">$${finalUsd.toFixed(2)} / ${finalBtc.toFixed(4)} BTC</span>
|
||||||
|
</div>
|
||||||
|
<div class="sim-stat-row">
|
||||||
|
<span>Trades (Entry / Exit)</span>
|
||||||
|
<span class="sim-value">${entryTrades} / ${exitTrades}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
|
||||||
|
<span style="font-size: 11px; color: var(--tv-text-secondary);">Equity Chart</span>
|
||||||
|
<div class="chart-toggle-group">
|
||||||
|
<button class="toggle-btn active" data-unit="usd">USD</button>
|
||||||
|
<button class="toggle-btn" data-unit="btc">BTC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="equity-chart-container" id="equityChart"></div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
|
||||||
|
<span style="font-size: 11px; color: var(--tv-text-secondary);" id="posSizeLabel">Position Size (BTC)</span>
|
||||||
|
<div class="chart-toggle-group">
|
||||||
|
<button class="toggle-btn active" data-unit="usd">USD</button>
|
||||||
|
<button class="toggle-btn" data-unit="btc">BTC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="equity-chart-container" id="posSizeChart"></div>
|
||||||
|
|
||||||
|
<div class="results-actions">
|
||||||
|
<button class="action-btn secondary" id="toggleTradeMarkers">Show Markers</button>
|
||||||
|
<button class="action-btn secondary" id="clearSim">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create Charts
|
||||||
|
const initCharts = () => {
|
||||||
|
const equityContainer = document.getElementById('equityChart');
|
||||||
|
if (equityContainer) {
|
||||||
|
equityContainer.innerHTML = '';
|
||||||
|
equityChart = LightweightCharts.createChart(equityContainer, {
|
||||||
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||||
|
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||||
|
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||||
|
timeScale: {
|
||||||
|
borderColor: '#2a2e39',
|
||||||
|
visible: true,
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: false,
|
||||||
|
tickMarkFormatter: (time, tickMarkType, locale) => {
|
||||||
|
return window.TimezoneConfig ? window.TimezoneConfig.formatTickMark(time) : new Date(time * 1000).toLocaleDateString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localization: {
|
||||||
|
timeFormatter: (timestamp) => {
|
||||||
|
return window.TimezoneConfig ? window.TimezoneConfig.formatDate(timestamp * 1000) : new Date(timestamp * 1000).toLocaleString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handleScroll: true,
|
||||||
|
handleScale: true
|
||||||
|
});
|
||||||
|
|
||||||
|
equitySeries = equityChart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
|
lineColor: totalPnlUsd >= 0 ? '#26a69a' : '#ef5350',
|
||||||
|
topColor: totalPnlUsd >= 0 ? 'rgba(38, 166, 154, 0.4)' : 'rgba(239, 83, 80, 0.4)',
|
||||||
|
bottomColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
equitySeries.setData(equityData['usd']);
|
||||||
|
equityChart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
const posSizeContainer = document.getElementById('posSizeChart');
|
||||||
|
if (posSizeContainer) {
|
||||||
|
posSizeContainer.innerHTML = '';
|
||||||
|
posSizeChart = LightweightCharts.createChart(posSizeContainer, {
|
||||||
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||||
|
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||||
|
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||||
|
timeScale: {
|
||||||
|
borderColor: '#2a2e39',
|
||||||
|
visible: true,
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: false,
|
||||||
|
tickMarkFormatter: (time, tickMarkType, locale) => {
|
||||||
|
return window.TimezoneConfig ? window.TimezoneConfig.formatTickMark(time) : new Date(time * 1000).toLocaleDateString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localization: {
|
||||||
|
timeFormatter: (timestamp) => {
|
||||||
|
return window.TimezoneConfig ? window.TimezoneConfig.formatDate(timestamp * 1000) : new Date(timestamp * 1000).toLocaleString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handleScroll: true,
|
||||||
|
handleScale: true
|
||||||
|
});
|
||||||
|
|
||||||
|
posSeries = posSizeChart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
|
lineColor: '#00bcd4',
|
||||||
|
topColor: 'rgba(0, 188, 212, 0.4)',
|
||||||
|
bottomColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
posSeries.setData(posSizeData['usd']);
|
||||||
|
posSizeChart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const label = document.getElementById('posSizeLabel');
|
||||||
|
if (label) label.textContent = 'Position Size (USD)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equityChart && posSizeChart) {
|
||||||
|
let isSyncing = false;
|
||||||
|
|
||||||
|
const syncCharts = (source, target) => {
|
||||||
|
if (isSyncing) return;
|
||||||
|
isSyncing = true;
|
||||||
|
const range = source.timeScale().getVisibleRange();
|
||||||
|
target.timeScale().setVisibleRange(range);
|
||||||
|
isSyncing = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
equityChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(equityChart, posSizeChart));
|
||||||
|
posSizeChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(posSizeChart, equityChart));
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncToMain = (param) => {
|
||||||
|
if (!param.time || !window.dashboard || !window.dashboard.chart) return;
|
||||||
|
|
||||||
|
const timeScale = window.dashboard.chart.timeScale();
|
||||||
|
const currentRange = timeScale.getVisibleRange();
|
||||||
|
if (!currentRange) return;
|
||||||
|
|
||||||
|
const width = currentRange.to - currentRange.from;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
|
||||||
|
timeScale.setVisibleRange({
|
||||||
|
from: param.time - halfWidth,
|
||||||
|
to: param.time + halfWidth
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (equityChart) equityChart.subscribeClick(syncToMain);
|
||||||
|
if (posSizeChart) posSizeChart.subscribeClick(syncToMain);
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(initCharts, 100);
|
||||||
|
|
||||||
|
resultsDiv.querySelectorAll('.toggle-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const unit = btn.dataset.unit;
|
||||||
|
|
||||||
|
resultsDiv.querySelectorAll(`.toggle-btn`).forEach(b => {
|
||||||
|
if (b.dataset.unit === unit) b.classList.add('active');
|
||||||
|
else b.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (equitySeries) {
|
||||||
|
equitySeries.setData(equityData[unit]);
|
||||||
|
equityChart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
if (posSeries) {
|
||||||
|
posSeries.setData(posSizeData[unit]);
|
||||||
|
posSizeChart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const label = document.getElementById('posSizeLabel');
|
||||||
|
if (label) label.textContent = `Position Size (${unit.toUpperCase()})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('toggleTradeMarkers').addEventListener('click', () => {
|
||||||
|
toggleSimulationMarkers(trades);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clearSim').addEventListener('click', () => {
|
||||||
|
resultsDiv.style.display = 'none';
|
||||||
|
clearSimulationMarkers();
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.clearAvgPriceData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSimulationMarkers(trades) {
|
||||||
|
if (tradeMarkers.length > 0) {
|
||||||
|
clearSimulationMarkers();
|
||||||
|
document.getElementById('toggleTradeMarkers').textContent = 'Show Markers';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const markers = [];
|
||||||
|
trades.forEach(t => {
|
||||||
|
const usdVal = t.currentUsd !== undefined ? `$${t.currentUsd.toFixed(0)}` : '0';
|
||||||
|
const qtyVal = t.currentQty !== undefined ? `${t.currentQty.toFixed(4)} BTC` : '0';
|
||||||
|
const sizeStr = ` (${usdVal} / ${qtyVal})`;
|
||||||
|
|
||||||
|
if (t.recordType === 'entry') {
|
||||||
|
markers.push({
|
||||||
|
time: t.time,
|
||||||
|
position: t.type === 'long' ? 'belowBar' : 'aboveBar',
|
||||||
|
color: t.type === 'long' ? '#2962ff' : '#9c27b0',
|
||||||
|
shape: t.type === 'long' ? 'arrowUp' : 'arrowDown',
|
||||||
|
text: `Entry ${t.type.toUpperCase()}${sizeStr}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.recordType === 'exit') {
|
||||||
|
markers.push({
|
||||||
|
time: t.time,
|
||||||
|
position: t.type === 'long' ? 'aboveBar' : 'belowBar',
|
||||||
|
color: t.pnl >= 0 ? '#26a69a' : '#ef5350',
|
||||||
|
shape: t.type === 'long' ? 'arrowDown' : 'arrowUp',
|
||||||
|
text: `Exit ${t.reason}${sizeStr}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markers.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.setSimulationMarkers(markers);
|
||||||
|
tradeMarkers = markers;
|
||||||
|
document.getElementById('toggleTradeMarkers').textContent = 'Hide Markers';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSimulationMarkers() {
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.clearSimulationMarkers();
|
||||||
|
tradeMarkers = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.clearSimulationResults = function() {
|
||||||
|
const resultsDiv = document.getElementById('simulationResults');
|
||||||
|
if (resultsDiv) resultsDiv.style.display = 'none';
|
||||||
|
clearSimulationMarkers();
|
||||||
|
};
|
||||||
23
js/utils/helpers.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export function downloadFile(content, filename, mimeType) {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date) {
|
||||||
|
return new Date(date).toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPrice(price, decimals = 2) {
|
||||||
|
return price.toFixed(decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPercent(value) {
|
||||||
|
return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
|
||||||
|
}
|
||||||
1
js/utils/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { downloadFile, formatDate, formatPrice, formatPercent } from './helpers.js';
|
||||||
349
new_design/ai_themed_trading_dashboard/code.html
Normal 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&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 & 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>
|
||||||
BIN
new_design/ai_themed_trading_dashboard/screen.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
301
new_design/optimized_trading_dashboard_view/code.html
Normal 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&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&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>
|
||||||
BIN
new_design/optimized_trading_dashboard_view/screen.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
108
new_design/product_requirements_document.md
Normal 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.
|
||||||
293
new_design/refined_trading_dashboard/code.html
Normal 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&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&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>
|
||||||
BIN
new_design/refined_trading_dashboard/screen.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
73
new_design/velocity_terminal/DESIGN.md
Normal 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. Do’s and Don’ts
|
||||||
|
|
||||||
|
### 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.
|
||||||
16
node_modules/.bin/he
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../he/bin/he" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../he/bin/he" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/he.cmd
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\he\bin\he" %*
|
||||||
28
node_modules/.bin/he.ps1
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../he/bin/he" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../he/bin/he" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../he/bin/he" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../he/bin/he" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
node_modules/.bin/http-server
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../http-server/bin/http-server" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../http-server/bin/http-server" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/http-server.cmd
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\http-server\bin\http-server" %*
|
||||||
28
node_modules/.bin/http-server.ps1
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../http-server/bin/http-server" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../http-server/bin/http-server" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../http-server/bin/http-server" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../http-server/bin/http-server" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
node_modules/.bin/mime
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../mime/cli.js" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/mime.cmd
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mime\cli.js" %*
|
||||||
28
node_modules/.bin/mime.ps1
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
node_modules/.bin/opener
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../opener/bin/opener-bin.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../opener/bin/opener-bin.js" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/opener.cmd
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\opener\bin\opener-bin.js" %*
|
||||||
28
node_modules/.bin/opener.ps1
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../opener/bin/opener-bin.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../opener/bin/opener-bin.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../opener/bin/opener-bin.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../opener/bin/opener-bin.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
1247
node_modules/.package-lock.json
generated
vendored
Normal file
243
node_modules/accepts/HISTORY.md
generated
vendored
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
1.3.8 / 2022-02-02
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.34
|
||||||
|
- deps: mime-db@~1.51.0
|
||||||
|
* deps: negotiator@0.6.3
|
||||||
|
|
||||||
|
1.3.7 / 2019-04-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.6.2
|
||||||
|
- Fix sorting charset, encoding, and language with extra parameters
|
||||||
|
|
||||||
|
1.3.6 / 2019-04-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.24
|
||||||
|
- deps: mime-db@~1.40.0
|
||||||
|
|
||||||
|
1.3.5 / 2018-02-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.18
|
||||||
|
- deps: mime-db@~1.33.0
|
||||||
|
|
||||||
|
1.3.4 / 2017-08-22
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.16
|
||||||
|
- deps: mime-db@~1.29.0
|
||||||
|
|
||||||
|
1.3.3 / 2016-05-02
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.11
|
||||||
|
- deps: mime-db@~1.23.0
|
||||||
|
* deps: negotiator@0.6.1
|
||||||
|
- perf: improve `Accept` parsing speed
|
||||||
|
- perf: improve `Accept-Charset` parsing speed
|
||||||
|
- perf: improve `Accept-Encoding` parsing speed
|
||||||
|
- perf: improve `Accept-Language` parsing speed
|
||||||
|
|
||||||
|
1.3.2 / 2016-03-08
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.10
|
||||||
|
- Fix extension of `application/dash+xml`
|
||||||
|
- Update primary extension for `audio/mp4`
|
||||||
|
- deps: mime-db@~1.22.0
|
||||||
|
|
||||||
|
1.3.1 / 2016-01-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.9
|
||||||
|
- deps: mime-db@~1.21.0
|
||||||
|
|
||||||
|
1.3.0 / 2015-09-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.7
|
||||||
|
- deps: mime-db@~1.19.0
|
||||||
|
* deps: negotiator@0.6.0
|
||||||
|
- Fix including type extensions in parameters in `Accept` parsing
|
||||||
|
- Fix parsing `Accept` parameters with quoted equals
|
||||||
|
- Fix parsing `Accept` parameters with quoted semicolons
|
||||||
|
- Lazy-load modules from main entry point
|
||||||
|
- perf: delay type concatenation until needed
|
||||||
|
- perf: enable strict mode
|
||||||
|
- perf: hoist regular expressions
|
||||||
|
- perf: remove closures getting spec properties
|
||||||
|
- perf: remove a closure from media type parsing
|
||||||
|
- perf: remove property delete from media type parsing
|
||||||
|
|
||||||
|
1.2.13 / 2015-09-06
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.6
|
||||||
|
- deps: mime-db@~1.18.0
|
||||||
|
|
||||||
|
1.2.12 / 2015-07-30
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.4
|
||||||
|
- deps: mime-db@~1.16.0
|
||||||
|
|
||||||
|
1.2.11 / 2015-07-16
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.3
|
||||||
|
- deps: mime-db@~1.15.0
|
||||||
|
|
||||||
|
1.2.10 / 2015-07-01
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.2
|
||||||
|
- deps: mime-db@~1.14.0
|
||||||
|
|
||||||
|
1.2.9 / 2015-06-08
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.1
|
||||||
|
- perf: fix deopt during mapping
|
||||||
|
|
||||||
|
1.2.8 / 2015-06-07
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.0
|
||||||
|
- deps: mime-db@~1.13.0
|
||||||
|
* perf: avoid argument reassignment & argument slice
|
||||||
|
* perf: avoid negotiator recursive construction
|
||||||
|
* perf: enable strict mode
|
||||||
|
* perf: remove unnecessary bitwise operator
|
||||||
|
|
||||||
|
1.2.7 / 2015-05-10
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.5.3
|
||||||
|
- Fix media type parameter matching to be case-insensitive
|
||||||
|
|
||||||
|
1.2.6 / 2015-05-07
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.11
|
||||||
|
- deps: mime-db@~1.9.1
|
||||||
|
* deps: negotiator@0.5.2
|
||||||
|
- Fix comparing media types with quoted values
|
||||||
|
- Fix splitting media types with quoted commas
|
||||||
|
|
||||||
|
1.2.5 / 2015-03-13
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.10
|
||||||
|
- deps: mime-db@~1.8.0
|
||||||
|
|
||||||
|
1.2.4 / 2015-02-14
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Support Node.js 0.6
|
||||||
|
* deps: mime-types@~2.0.9
|
||||||
|
- deps: mime-db@~1.7.0
|
||||||
|
* deps: negotiator@0.5.1
|
||||||
|
- Fix preference sorting to be stable for long acceptable lists
|
||||||
|
|
||||||
|
1.2.3 / 2015-01-31
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.8
|
||||||
|
- deps: mime-db@~1.6.0
|
||||||
|
|
||||||
|
1.2.2 / 2014-12-30
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.7
|
||||||
|
- deps: mime-db@~1.5.0
|
||||||
|
|
||||||
|
1.2.1 / 2014-12-30
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.5
|
||||||
|
- deps: mime-db@~1.3.1
|
||||||
|
|
||||||
|
1.2.0 / 2014-12-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.5.0
|
||||||
|
- Fix list return order when large accepted list
|
||||||
|
- Fix missing identity encoding when q=0 exists
|
||||||
|
- Remove dynamic building of Negotiator class
|
||||||
|
|
||||||
|
1.1.4 / 2014-12-10
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.4
|
||||||
|
- deps: mime-db@~1.3.0
|
||||||
|
|
||||||
|
1.1.3 / 2014-11-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.3
|
||||||
|
- deps: mime-db@~1.2.0
|
||||||
|
|
||||||
|
1.1.2 / 2014-10-14
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.4.9
|
||||||
|
- Fix error when media type has invalid parameter
|
||||||
|
|
||||||
|
1.1.1 / 2014-09-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.2
|
||||||
|
- deps: mime-db@~1.1.0
|
||||||
|
* deps: negotiator@0.4.8
|
||||||
|
- Fix all negotiations to be case-insensitive
|
||||||
|
- Stable sort preferences of same quality according to client order
|
||||||
|
|
||||||
|
1.1.0 / 2014-09-02
|
||||||
|
==================
|
||||||
|
|
||||||
|
* update `mime-types`
|
||||||
|
|
||||||
|
1.0.7 / 2014-07-04
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix wrong type returned from `type` when match after unknown extension
|
||||||
|
|
||||||
|
1.0.6 / 2014-06-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.4.7
|
||||||
|
|
||||||
|
1.0.5 / 2014-06-20
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fix crash when unknown extension given
|
||||||
|
|
||||||
|
1.0.4 / 2014-06-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* use `mime-types`
|
||||||
|
|
||||||
|
1.0.3 / 2014-06-11
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.4.6
|
||||||
|
- Order by specificity when quality is the same
|
||||||
|
|
||||||
|
1.0.2 / 2014-05-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix interpretation when header not in request
|
||||||
|
* deps: pin negotiator@0.4.5
|
||||||
|
|
||||||
|
1.0.1 / 2014-01-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Identity encoding isn't always acceptable
|
||||||
|
* deps: negotiator@~0.4.0
|
||||||
|
|
||||||
|
1.0.0 / 2013-12-27
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Genesis
|
||||||
23
node_modules/accepts/LICENSE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||||
|
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
140
node_modules/accepts/README.md
generated
vendored
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# accepts
|
||||||
|
|
||||||
|
[![NPM Version][npm-version-image]][npm-url]
|
||||||
|
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||||
|
[![Node.js Version][node-version-image]][node-version-url]
|
||||||
|
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
||||||
|
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator).
|
||||||
|
Extracted from [koa](https://www.npmjs.com/package/koa) for general use.
|
||||||
|
|
||||||
|
In addition to negotiator, it allows:
|
||||||
|
|
||||||
|
- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])`
|
||||||
|
as well as `('text/html', 'application/json')`.
|
||||||
|
- Allows type shorthands such as `json`.
|
||||||
|
- Returns `false` when no types match
|
||||||
|
- Treats non-existent headers as `*`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||||
|
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||||
|
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install accepts
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
var accepts = require('accepts')
|
||||||
|
```
|
||||||
|
|
||||||
|
### accepts(req)
|
||||||
|
|
||||||
|
Create a new `Accepts` object for the given `req`.
|
||||||
|
|
||||||
|
#### .charset(charsets)
|
||||||
|
|
||||||
|
Return the first accepted charset. If nothing in `charsets` is accepted,
|
||||||
|
then `false` is returned.
|
||||||
|
|
||||||
|
#### .charsets()
|
||||||
|
|
||||||
|
Return the charsets that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
#### .encoding(encodings)
|
||||||
|
|
||||||
|
Return the first accepted encoding. If nothing in `encodings` is accepted,
|
||||||
|
then `false` is returned.
|
||||||
|
|
||||||
|
#### .encodings()
|
||||||
|
|
||||||
|
Return the encodings that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
#### .language(languages)
|
||||||
|
|
||||||
|
Return the first accepted language. If nothing in `languages` is accepted,
|
||||||
|
then `false` is returned.
|
||||||
|
|
||||||
|
#### .languages()
|
||||||
|
|
||||||
|
Return the languages that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
#### .type(types)
|
||||||
|
|
||||||
|
Return the first accepted type (and it is returned as the same text as what
|
||||||
|
appears in the `types` array). If nothing in `types` is accepted, then `false`
|
||||||
|
is returned.
|
||||||
|
|
||||||
|
The `types` array can contain full MIME types or file extensions. Any value
|
||||||
|
that is not a full MIME types is passed to `require('mime-types').lookup`.
|
||||||
|
|
||||||
|
#### .types()
|
||||||
|
|
||||||
|
Return the types that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Simple type negotiation
|
||||||
|
|
||||||
|
This simple example shows how to use `accepts` to return a different typed
|
||||||
|
respond body based on what the client wants to accept. The server lists it's
|
||||||
|
preferences in order and will get back the best match between the client and
|
||||||
|
server.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var accepts = require('accepts')
|
||||||
|
var http = require('http')
|
||||||
|
|
||||||
|
function app (req, res) {
|
||||||
|
var accept = accepts(req)
|
||||||
|
|
||||||
|
// the order of this list is significant; should be server preferred order
|
||||||
|
switch (accept.type(['json', 'html'])) {
|
||||||
|
case 'json':
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.write('{"hello":"world!"}')
|
||||||
|
break
|
||||||
|
case 'html':
|
||||||
|
res.setHeader('Content-Type', 'text/html')
|
||||||
|
res.write('<b>hello, world!</b>')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// the fallback is text/plain, so no need to specify it above
|
||||||
|
res.setHeader('Content-Type', 'text/plain')
|
||||||
|
res.write('hello, world!')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
http.createServer(app).listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can test this out with the cURL program:
|
||||||
|
```sh
|
||||||
|
curl -I -H'Accept: text/html' http://localhost:3000/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master
|
||||||
|
[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master
|
||||||
|
[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci
|
||||||
|
[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml
|
||||||
|
[node-version-image]: https://badgen.net/npm/node/accepts
|
||||||
|
[node-version-url]: https://nodejs.org/en/download
|
||||||
|
[npm-downloads-image]: https://badgen.net/npm/dm/accepts
|
||||||
|
[npm-url]: https://npmjs.org/package/accepts
|
||||||
|
[npm-version-image]: https://badgen.net/npm/v/accepts
|
||||||
238
node_modules/accepts/index.js
generated
vendored
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
/*!
|
||||||
|
* accepts
|
||||||
|
* Copyright(c) 2014 Jonathan Ong
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Negotiator = require('negotiator')
|
||||||
|
var mime = require('mime-types')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = Accepts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Accepts object for the given req.
|
||||||
|
*
|
||||||
|
* @param {object} req
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Accepts (req) {
|
||||||
|
if (!(this instanceof Accepts)) {
|
||||||
|
return new Accepts(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.headers = req.headers
|
||||||
|
this.negotiator = new Negotiator(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given `type(s)` is acceptable, returning
|
||||||
|
* the best match when true, otherwise `undefined`, in which
|
||||||
|
* case you should respond with 406 "Not Acceptable".
|
||||||
|
*
|
||||||
|
* The `type` value may be a single mime type string
|
||||||
|
* such as "application/json", the extension name
|
||||||
|
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
||||||
|
* or array is given the _best_ match, if any is returned.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* // Accept: text/html
|
||||||
|
* this.types('html');
|
||||||
|
* // => "html"
|
||||||
|
*
|
||||||
|
* // Accept: text/*, application/json
|
||||||
|
* this.types('html');
|
||||||
|
* // => "html"
|
||||||
|
* this.types('text/html');
|
||||||
|
* // => "text/html"
|
||||||
|
* this.types('json', 'text');
|
||||||
|
* // => "json"
|
||||||
|
* this.types('application/json');
|
||||||
|
* // => "application/json"
|
||||||
|
*
|
||||||
|
* // Accept: text/*, application/json
|
||||||
|
* this.types('image/png');
|
||||||
|
* this.types('png');
|
||||||
|
* // => undefined
|
||||||
|
*
|
||||||
|
* // Accept: text/*;q=.5, application/json
|
||||||
|
* this.types(['html', 'json']);
|
||||||
|
* this.types('html', 'json');
|
||||||
|
* // => "json"
|
||||||
|
*
|
||||||
|
* @param {String|Array} types...
|
||||||
|
* @return {String|Array|Boolean}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.type =
|
||||||
|
Accepts.prototype.types = function (types_) {
|
||||||
|
var types = types_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (types && !Array.isArray(types)) {
|
||||||
|
types = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < types.length; i++) {
|
||||||
|
types[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no types, return all requested types
|
||||||
|
if (!types || types.length === 0) {
|
||||||
|
return this.negotiator.mediaTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no accept header, return first given type
|
||||||
|
if (!this.headers.accept) {
|
||||||
|
return types[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimes = types.map(extToMime)
|
||||||
|
var accepts = this.negotiator.mediaTypes(mimes.filter(validMime))
|
||||||
|
var first = accepts[0]
|
||||||
|
|
||||||
|
return first
|
||||||
|
? types[mimes.indexOf(first)]
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted encodings or best fit based on `encodings`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Encoding: gzip, deflate`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['gzip', 'deflate']
|
||||||
|
*
|
||||||
|
* @param {String|Array} encodings...
|
||||||
|
* @return {String|Array}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.encoding =
|
||||||
|
Accepts.prototype.encodings = function (encodings_) {
|
||||||
|
var encodings = encodings_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (encodings && !Array.isArray(encodings)) {
|
||||||
|
encodings = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < encodings.length; i++) {
|
||||||
|
encodings[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no encodings, return all requested encodings
|
||||||
|
if (!encodings || encodings.length === 0) {
|
||||||
|
return this.negotiator.encodings()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.negotiator.encodings(encodings)[0] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted charsets or best fit based on `charsets`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['utf-8', 'utf-7', 'iso-8859-1']
|
||||||
|
*
|
||||||
|
* @param {String|Array} charsets...
|
||||||
|
* @return {String|Array}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.charset =
|
||||||
|
Accepts.prototype.charsets = function (charsets_) {
|
||||||
|
var charsets = charsets_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (charsets && !Array.isArray(charsets)) {
|
||||||
|
charsets = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < charsets.length; i++) {
|
||||||
|
charsets[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no charsets, return all requested charsets
|
||||||
|
if (!charsets || charsets.length === 0) {
|
||||||
|
return this.negotiator.charsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.negotiator.charsets(charsets)[0] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted languages or best fit based on `langs`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Language: en;q=0.8, es, pt`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['es', 'pt', 'en']
|
||||||
|
*
|
||||||
|
* @param {String|Array} langs...
|
||||||
|
* @return {Array|String}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.lang =
|
||||||
|
Accepts.prototype.langs =
|
||||||
|
Accepts.prototype.language =
|
||||||
|
Accepts.prototype.languages = function (languages_) {
|
||||||
|
var languages = languages_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (languages && !Array.isArray(languages)) {
|
||||||
|
languages = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < languages.length; i++) {
|
||||||
|
languages[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no languages, return all requested languages
|
||||||
|
if (!languages || languages.length === 0) {
|
||||||
|
return this.negotiator.languages()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.negotiator.languages(languages)[0] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert extnames to mime.
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @return {String}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function extToMime (type) {
|
||||||
|
return type.indexOf('/') === -1
|
||||||
|
? mime.lookup(type)
|
||||||
|
: type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if mime is valid.
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @return {String}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function validMime (type) {
|
||||||
|
return typeof type === 'string'
|
||||||
|
}
|
||||||
47
node_modules/accepts/package.json
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "accepts",
|
||||||
|
"description": "Higher-level content negotiation",
|
||||||
|
"version": "1.3.8",
|
||||||
|
"contributors": [
|
||||||
|
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||||
|
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "jshttp/accepts",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"deep-equal": "1.0.1",
|
||||||
|
"eslint": "7.32.0",
|
||||||
|
"eslint-config-standard": "14.1.1",
|
||||||
|
"eslint-plugin-import": "2.25.4",
|
||||||
|
"eslint-plugin-markdown": "2.2.1",
|
||||||
|
"eslint-plugin-node": "11.1.0",
|
||||||
|
"eslint-plugin-promise": "4.3.1",
|
||||||
|
"eslint-plugin-standard": "4.1.0",
|
||||||
|
"mocha": "9.2.0",
|
||||||
|
"nyc": "15.1.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"LICENSE",
|
||||||
|
"HISTORY.md",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "mocha --reporter spec --check-leaks --bail test/",
|
||||||
|
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||||
|
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"content",
|
||||||
|
"negotiation",
|
||||||
|
"accept",
|
||||||
|
"accepts"
|
||||||
|
]
|
||||||
|
}
|
||||||
345
node_modules/ansi-styles/index.d.ts
generated
vendored
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
declare type CSSColor =
|
||||||
|
| 'aliceblue'
|
||||||
|
| 'antiquewhite'
|
||||||
|
| 'aqua'
|
||||||
|
| 'aquamarine'
|
||||||
|
| 'azure'
|
||||||
|
| 'beige'
|
||||||
|
| 'bisque'
|
||||||
|
| 'black'
|
||||||
|
| 'blanchedalmond'
|
||||||
|
| 'blue'
|
||||||
|
| 'blueviolet'
|
||||||
|
| 'brown'
|
||||||
|
| 'burlywood'
|
||||||
|
| 'cadetblue'
|
||||||
|
| 'chartreuse'
|
||||||
|
| 'chocolate'
|
||||||
|
| 'coral'
|
||||||
|
| 'cornflowerblue'
|
||||||
|
| 'cornsilk'
|
||||||
|
| 'crimson'
|
||||||
|
| 'cyan'
|
||||||
|
| 'darkblue'
|
||||||
|
| 'darkcyan'
|
||||||
|
| 'darkgoldenrod'
|
||||||
|
| 'darkgray'
|
||||||
|
| 'darkgreen'
|
||||||
|
| 'darkgrey'
|
||||||
|
| 'darkkhaki'
|
||||||
|
| 'darkmagenta'
|
||||||
|
| 'darkolivegreen'
|
||||||
|
| 'darkorange'
|
||||||
|
| 'darkorchid'
|
||||||
|
| 'darkred'
|
||||||
|
| 'darksalmon'
|
||||||
|
| 'darkseagreen'
|
||||||
|
| 'darkslateblue'
|
||||||
|
| 'darkslategray'
|
||||||
|
| 'darkslategrey'
|
||||||
|
| 'darkturquoise'
|
||||||
|
| 'darkviolet'
|
||||||
|
| 'deeppink'
|
||||||
|
| 'deepskyblue'
|
||||||
|
| 'dimgray'
|
||||||
|
| 'dimgrey'
|
||||||
|
| 'dodgerblue'
|
||||||
|
| 'firebrick'
|
||||||
|
| 'floralwhite'
|
||||||
|
| 'forestgreen'
|
||||||
|
| 'fuchsia'
|
||||||
|
| 'gainsboro'
|
||||||
|
| 'ghostwhite'
|
||||||
|
| 'gold'
|
||||||
|
| 'goldenrod'
|
||||||
|
| 'gray'
|
||||||
|
| 'green'
|
||||||
|
| 'greenyellow'
|
||||||
|
| 'grey'
|
||||||
|
| 'honeydew'
|
||||||
|
| 'hotpink'
|
||||||
|
| 'indianred'
|
||||||
|
| 'indigo'
|
||||||
|
| 'ivory'
|
||||||
|
| 'khaki'
|
||||||
|
| 'lavender'
|
||||||
|
| 'lavenderblush'
|
||||||
|
| 'lawngreen'
|
||||||
|
| 'lemonchiffon'
|
||||||
|
| 'lightblue'
|
||||||
|
| 'lightcoral'
|
||||||
|
| 'lightcyan'
|
||||||
|
| 'lightgoldenrodyellow'
|
||||||
|
| 'lightgray'
|
||||||
|
| 'lightgreen'
|
||||||
|
| 'lightgrey'
|
||||||
|
| 'lightpink'
|
||||||
|
| 'lightsalmon'
|
||||||
|
| 'lightseagreen'
|
||||||
|
| 'lightskyblue'
|
||||||
|
| 'lightslategray'
|
||||||
|
| 'lightslategrey'
|
||||||
|
| 'lightsteelblue'
|
||||||
|
| 'lightyellow'
|
||||||
|
| 'lime'
|
||||||
|
| 'limegreen'
|
||||||
|
| 'linen'
|
||||||
|
| 'magenta'
|
||||||
|
| 'maroon'
|
||||||
|
| 'mediumaquamarine'
|
||||||
|
| 'mediumblue'
|
||||||
|
| 'mediumorchid'
|
||||||
|
| 'mediumpurple'
|
||||||
|
| 'mediumseagreen'
|
||||||
|
| 'mediumslateblue'
|
||||||
|
| 'mediumspringgreen'
|
||||||
|
| 'mediumturquoise'
|
||||||
|
| 'mediumvioletred'
|
||||||
|
| 'midnightblue'
|
||||||
|
| 'mintcream'
|
||||||
|
| 'mistyrose'
|
||||||
|
| 'moccasin'
|
||||||
|
| 'navajowhite'
|
||||||
|
| 'navy'
|
||||||
|
| 'oldlace'
|
||||||
|
| 'olive'
|
||||||
|
| 'olivedrab'
|
||||||
|
| 'orange'
|
||||||
|
| 'orangered'
|
||||||
|
| 'orchid'
|
||||||
|
| 'palegoldenrod'
|
||||||
|
| 'palegreen'
|
||||||
|
| 'paleturquoise'
|
||||||
|
| 'palevioletred'
|
||||||
|
| 'papayawhip'
|
||||||
|
| 'peachpuff'
|
||||||
|
| 'peru'
|
||||||
|
| 'pink'
|
||||||
|
| 'plum'
|
||||||
|
| 'powderblue'
|
||||||
|
| 'purple'
|
||||||
|
| 'rebeccapurple'
|
||||||
|
| 'red'
|
||||||
|
| 'rosybrown'
|
||||||
|
| 'royalblue'
|
||||||
|
| 'saddlebrown'
|
||||||
|
| 'salmon'
|
||||||
|
| 'sandybrown'
|
||||||
|
| 'seagreen'
|
||||||
|
| 'seashell'
|
||||||
|
| 'sienna'
|
||||||
|
| 'silver'
|
||||||
|
| 'skyblue'
|
||||||
|
| 'slateblue'
|
||||||
|
| 'slategray'
|
||||||
|
| 'slategrey'
|
||||||
|
| 'snow'
|
||||||
|
| 'springgreen'
|
||||||
|
| 'steelblue'
|
||||||
|
| 'tan'
|
||||||
|
| 'teal'
|
||||||
|
| 'thistle'
|
||||||
|
| 'tomato'
|
||||||
|
| 'turquoise'
|
||||||
|
| 'violet'
|
||||||
|
| 'wheat'
|
||||||
|
| 'white'
|
||||||
|
| 'whitesmoke'
|
||||||
|
| 'yellow'
|
||||||
|
| 'yellowgreen';
|
||||||
|
|
||||||
|
declare namespace ansiStyles {
|
||||||
|
interface ColorConvert {
|
||||||
|
/**
|
||||||
|
The RGB color space.
|
||||||
|
|
||||||
|
@param red - (`0`-`255`)
|
||||||
|
@param green - (`0`-`255`)
|
||||||
|
@param blue - (`0`-`255`)
|
||||||
|
*/
|
||||||
|
rgb(red: number, green: number, blue: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
The RGB HEX color space.
|
||||||
|
|
||||||
|
@param hex - A hexadecimal string containing RGB data.
|
||||||
|
*/
|
||||||
|
hex(hex: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param keyword - A CSS color name.
|
||||||
|
*/
|
||||||
|
keyword(keyword: CSSColor): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
The HSL color space.
|
||||||
|
|
||||||
|
@param hue - (`0`-`360`)
|
||||||
|
@param saturation - (`0`-`100`)
|
||||||
|
@param lightness - (`0`-`100`)
|
||||||
|
*/
|
||||||
|
hsl(hue: number, saturation: number, lightness: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
The HSV color space.
|
||||||
|
|
||||||
|
@param hue - (`0`-`360`)
|
||||||
|
@param saturation - (`0`-`100`)
|
||||||
|
@param value - (`0`-`100`)
|
||||||
|
*/
|
||||||
|
hsv(hue: number, saturation: number, value: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
The HSV color space.
|
||||||
|
|
||||||
|
@param hue - (`0`-`360`)
|
||||||
|
@param whiteness - (`0`-`100`)
|
||||||
|
@param blackness - (`0`-`100`)
|
||||||
|
*/
|
||||||
|
hwb(hue: number, whiteness: number, blackness: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Use a [4-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4-bit) to set text color.
|
||||||
|
*/
|
||||||
|
ansi(ansi: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
|
||||||
|
*/
|
||||||
|
ansi256(ansi: number): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CSPair {
|
||||||
|
/**
|
||||||
|
The ANSI terminal control sequence for starting this style.
|
||||||
|
*/
|
||||||
|
readonly open: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
The ANSI terminal control sequence for ending this style.
|
||||||
|
*/
|
||||||
|
readonly close: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorBase {
|
||||||
|
readonly ansi: ColorConvert;
|
||||||
|
readonly ansi256: ColorConvert;
|
||||||
|
readonly ansi16m: ColorConvert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
The ANSI terminal control sequence for ending this color.
|
||||||
|
*/
|
||||||
|
readonly close: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Modifier {
|
||||||
|
/**
|
||||||
|
Resets the current color chain.
|
||||||
|
*/
|
||||||
|
readonly reset: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Make text bold.
|
||||||
|
*/
|
||||||
|
readonly bold: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Emitting only a small amount of light.
|
||||||
|
*/
|
||||||
|
readonly dim: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Make text italic. (Not widely supported)
|
||||||
|
*/
|
||||||
|
readonly italic: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Make text underline. (Not widely supported)
|
||||||
|
*/
|
||||||
|
readonly underline: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Inverse background and foreground colors.
|
||||||
|
*/
|
||||||
|
readonly inverse: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Prints the text, but makes it invisible.
|
||||||
|
*/
|
||||||
|
readonly hidden: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Puts a horizontal line through the center of the text. (Not widely supported)
|
||||||
|
*/
|
||||||
|
readonly strikethrough: CSPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForegroundColor {
|
||||||
|
readonly black: CSPair;
|
||||||
|
readonly red: CSPair;
|
||||||
|
readonly green: CSPair;
|
||||||
|
readonly yellow: CSPair;
|
||||||
|
readonly blue: CSPair;
|
||||||
|
readonly cyan: CSPair;
|
||||||
|
readonly magenta: CSPair;
|
||||||
|
readonly white: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Alias for `blackBright`.
|
||||||
|
*/
|
||||||
|
readonly gray: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Alias for `blackBright`.
|
||||||
|
*/
|
||||||
|
readonly grey: CSPair;
|
||||||
|
|
||||||
|
readonly blackBright: CSPair;
|
||||||
|
readonly redBright: CSPair;
|
||||||
|
readonly greenBright: CSPair;
|
||||||
|
readonly yellowBright: CSPair;
|
||||||
|
readonly blueBright: CSPair;
|
||||||
|
readonly cyanBright: CSPair;
|
||||||
|
readonly magentaBright: CSPair;
|
||||||
|
readonly whiteBright: CSPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackgroundColor {
|
||||||
|
readonly bgBlack: CSPair;
|
||||||
|
readonly bgRed: CSPair;
|
||||||
|
readonly bgGreen: CSPair;
|
||||||
|
readonly bgYellow: CSPair;
|
||||||
|
readonly bgBlue: CSPair;
|
||||||
|
readonly bgCyan: CSPair;
|
||||||
|
readonly bgMagenta: CSPair;
|
||||||
|
readonly bgWhite: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Alias for `bgBlackBright`.
|
||||||
|
*/
|
||||||
|
readonly bgGray: CSPair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Alias for `bgBlackBright`.
|
||||||
|
*/
|
||||||
|
readonly bgGrey: CSPair;
|
||||||
|
|
||||||
|
readonly bgBlackBright: CSPair;
|
||||||
|
readonly bgRedBright: CSPair;
|
||||||
|
readonly bgGreenBright: CSPair;
|
||||||
|
readonly bgYellowBright: CSPair;
|
||||||
|
readonly bgBlueBright: CSPair;
|
||||||
|
readonly bgCyanBright: CSPair;
|
||||||
|
readonly bgMagentaBright: CSPair;
|
||||||
|
readonly bgWhiteBright: CSPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const ansiStyles: {
|
||||||
|
readonly modifier: ansiStyles.Modifier;
|
||||||
|
readonly color: ansiStyles.ForegroundColor & ansiStyles.ColorBase;
|
||||||
|
readonly bgColor: ansiStyles.BackgroundColor & ansiStyles.ColorBase;
|
||||||
|
readonly codes: ReadonlyMap<number, number>;
|
||||||
|
} & ansiStyles.BackgroundColor & ansiStyles.ForegroundColor & ansiStyles.Modifier;
|
||||||
|
|
||||||
|
export = ansiStyles;
|
||||||
163
node_modules/ansi-styles/index.js
generated
vendored
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const wrapAnsi16 = (fn, offset) => (...args) => {
|
||||||
|
const code = fn(...args);
|
||||||
|
return `\u001B[${code + offset}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapAnsi256 = (fn, offset) => (...args) => {
|
||||||
|
const code = fn(...args);
|
||||||
|
return `\u001B[${38 + offset};5;${code}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapAnsi16m = (fn, offset) => (...args) => {
|
||||||
|
const rgb = fn(...args);
|
||||||
|
return `\u001B[${38 + offset};2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ansi2ansi = n => n;
|
||||||
|
const rgb2rgb = (r, g, b) => [r, g, b];
|
||||||
|
|
||||||
|
const setLazyProperty = (object, property, get) => {
|
||||||
|
Object.defineProperty(object, property, {
|
||||||
|
get: () => {
|
||||||
|
const value = get();
|
||||||
|
|
||||||
|
Object.defineProperty(object, property, {
|
||||||
|
value,
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {typeof import('color-convert')} */
|
||||||
|
let colorConvert;
|
||||||
|
const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => {
|
||||||
|
if (colorConvert === undefined) {
|
||||||
|
colorConvert = require('color-convert');
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = isBackground ? 10 : 0;
|
||||||
|
const styles = {};
|
||||||
|
|
||||||
|
for (const [sourceSpace, suite] of Object.entries(colorConvert)) {
|
||||||
|
const name = sourceSpace === 'ansi16' ? 'ansi' : sourceSpace;
|
||||||
|
if (sourceSpace === targetSpace) {
|
||||||
|
styles[name] = wrap(identity, offset);
|
||||||
|
} else if (typeof suite === 'object') {
|
||||||
|
styles[name] = wrap(suite[targetSpace], offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles;
|
||||||
|
};
|
||||||
|
|
||||||
|
function assembleStyles() {
|
||||||
|
const codes = new Map();
|
||||||
|
const styles = {
|
||||||
|
modifier: {
|
||||||
|
reset: [0, 0],
|
||||||
|
// 21 isn't widely supported and 22 does the same thing
|
||||||
|
bold: [1, 22],
|
||||||
|
dim: [2, 22],
|
||||||
|
italic: [3, 23],
|
||||||
|
underline: [4, 24],
|
||||||
|
inverse: [7, 27],
|
||||||
|
hidden: [8, 28],
|
||||||
|
strikethrough: [9, 29]
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
black: [30, 39],
|
||||||
|
red: [31, 39],
|
||||||
|
green: [32, 39],
|
||||||
|
yellow: [33, 39],
|
||||||
|
blue: [34, 39],
|
||||||
|
magenta: [35, 39],
|
||||||
|
cyan: [36, 39],
|
||||||
|
white: [37, 39],
|
||||||
|
|
||||||
|
// Bright color
|
||||||
|
blackBright: [90, 39],
|
||||||
|
redBright: [91, 39],
|
||||||
|
greenBright: [92, 39],
|
||||||
|
yellowBright: [93, 39],
|
||||||
|
blueBright: [94, 39],
|
||||||
|
magentaBright: [95, 39],
|
||||||
|
cyanBright: [96, 39],
|
||||||
|
whiteBright: [97, 39]
|
||||||
|
},
|
||||||
|
bgColor: {
|
||||||
|
bgBlack: [40, 49],
|
||||||
|
bgRed: [41, 49],
|
||||||
|
bgGreen: [42, 49],
|
||||||
|
bgYellow: [43, 49],
|
||||||
|
bgBlue: [44, 49],
|
||||||
|
bgMagenta: [45, 49],
|
||||||
|
bgCyan: [46, 49],
|
||||||
|
bgWhite: [47, 49],
|
||||||
|
|
||||||
|
// Bright color
|
||||||
|
bgBlackBright: [100, 49],
|
||||||
|
bgRedBright: [101, 49],
|
||||||
|
bgGreenBright: [102, 49],
|
||||||
|
bgYellowBright: [103, 49],
|
||||||
|
bgBlueBright: [104, 49],
|
||||||
|
bgMagentaBright: [105, 49],
|
||||||
|
bgCyanBright: [106, 49],
|
||||||
|
bgWhiteBright: [107, 49]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alias bright black as gray (and grey)
|
||||||
|
styles.color.gray = styles.color.blackBright;
|
||||||
|
styles.bgColor.bgGray = styles.bgColor.bgBlackBright;
|
||||||
|
styles.color.grey = styles.color.blackBright;
|
||||||
|
styles.bgColor.bgGrey = styles.bgColor.bgBlackBright;
|
||||||
|
|
||||||
|
for (const [groupName, group] of Object.entries(styles)) {
|
||||||
|
for (const [styleName, style] of Object.entries(group)) {
|
||||||
|
styles[styleName] = {
|
||||||
|
open: `\u001B[${style[0]}m`,
|
||||||
|
close: `\u001B[${style[1]}m`
|
||||||
|
};
|
||||||
|
|
||||||
|
group[styleName] = styles[styleName];
|
||||||
|
|
||||||
|
codes.set(style[0], style[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(styles, groupName, {
|
||||||
|
value: group,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(styles, 'codes', {
|
||||||
|
value: codes,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
styles.color.close = '\u001B[39m';
|
||||||
|
styles.bgColor.close = '\u001B[49m';
|
||||||
|
|
||||||
|
setLazyProperty(styles.color, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, false));
|
||||||
|
setLazyProperty(styles.color, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, false));
|
||||||
|
setLazyProperty(styles.color, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, false));
|
||||||
|
setLazyProperty(styles.bgColor, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, true));
|
||||||
|
setLazyProperty(styles.bgColor, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, true));
|
||||||
|
setLazyProperty(styles.bgColor, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, true));
|
||||||
|
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the export immutable
|
||||||
|
Object.defineProperty(module, 'exports', {
|
||||||
|
enumerable: true,
|
||||||
|
get: assembleStyles
|
||||||
|
});
|
||||||
9
node_modules/ansi-styles/license
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
56
node_modules/ansi-styles/package.json
generated
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "ansi-styles",
|
||||||
|
"version": "4.3.0",
|
||||||
|
"description": "ANSI escape codes for styling strings in the terminal",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "chalk/ansi-styles",
|
||||||
|
"funding": "https://github.com/chalk/ansi-styles?sponsor=1",
|
||||||
|
"author": {
|
||||||
|
"name": "Sindre Sorhus",
|
||||||
|
"email": "sindresorhus@gmail.com",
|
||||||
|
"url": "sindresorhus.com"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "xo && ava && tsd",
|
||||||
|
"screenshot": "svg-term --command='node screenshot' --out=screenshot.svg --padding=3 --width=55 --height=3 --at=1000 --no-cursor"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"index.d.ts"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"ansi",
|
||||||
|
"styles",
|
||||||
|
"color",
|
||||||
|
"colour",
|
||||||
|
"colors",
|
||||||
|
"terminal",
|
||||||
|
"console",
|
||||||
|
"cli",
|
||||||
|
"string",
|
||||||
|
"tty",
|
||||||
|
"escape",
|
||||||
|
"formatting",
|
||||||
|
"rgb",
|
||||||
|
"256",
|
||||||
|
"shell",
|
||||||
|
"xterm",
|
||||||
|
"log",
|
||||||
|
"logging",
|
||||||
|
"command-line",
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/color-convert": "^1.9.0",
|
||||||
|
"ava": "^2.3.0",
|
||||||
|
"svg-term-cli": "^2.1.1",
|
||||||
|
"tsd": "^0.11.0",
|
||||||
|
"xo": "^0.25.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
152
node_modules/ansi-styles/readme.md
generated
vendored
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# ansi-styles [](https://travis-ci.org/chalk/ansi-styles)
|
||||||
|
|
||||||
|
> [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles) for styling strings in the terminal
|
||||||
|
|
||||||
|
You probably want the higher-level [chalk](https://github.com/chalk/chalk) module for styling your strings.
|
||||||
|
|
||||||
|
<img src="screenshot.svg" width="900">
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm install ansi-styles
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
const style = require('ansi-styles');
|
||||||
|
|
||||||
|
console.log(`${style.green.open}Hello world!${style.green.close}`);
|
||||||
|
|
||||||
|
|
||||||
|
// Color conversion between 16/256/truecolor
|
||||||
|
// NOTE: If conversion goes to 16 colors or 256 colors, the original color
|
||||||
|
// may be degraded to fit that color palette. This means terminals
|
||||||
|
// that do not support 16 million colors will best-match the
|
||||||
|
// original color.
|
||||||
|
console.log(style.bgColor.ansi.hsl(120, 80, 72) + 'Hello world!' + style.bgColor.close);
|
||||||
|
console.log(style.color.ansi256.rgb(199, 20, 250) + 'Hello world!' + style.color.close);
|
||||||
|
console.log(style.color.ansi16m.hex('#abcdef') + 'Hello world!' + style.color.close);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Each style has an `open` and `close` property.
|
||||||
|
|
||||||
|
## Styles
|
||||||
|
|
||||||
|
### Modifiers
|
||||||
|
|
||||||
|
- `reset`
|
||||||
|
- `bold`
|
||||||
|
- `dim`
|
||||||
|
- `italic` *(Not widely supported)*
|
||||||
|
- `underline`
|
||||||
|
- `inverse`
|
||||||
|
- `hidden`
|
||||||
|
- `strikethrough` *(Not widely supported)*
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
- `black`
|
||||||
|
- `red`
|
||||||
|
- `green`
|
||||||
|
- `yellow`
|
||||||
|
- `blue`
|
||||||
|
- `magenta`
|
||||||
|
- `cyan`
|
||||||
|
- `white`
|
||||||
|
- `blackBright` (alias: `gray`, `grey`)
|
||||||
|
- `redBright`
|
||||||
|
- `greenBright`
|
||||||
|
- `yellowBright`
|
||||||
|
- `blueBright`
|
||||||
|
- `magentaBright`
|
||||||
|
- `cyanBright`
|
||||||
|
- `whiteBright`
|
||||||
|
|
||||||
|
### Background colors
|
||||||
|
|
||||||
|
- `bgBlack`
|
||||||
|
- `bgRed`
|
||||||
|
- `bgGreen`
|
||||||
|
- `bgYellow`
|
||||||
|
- `bgBlue`
|
||||||
|
- `bgMagenta`
|
||||||
|
- `bgCyan`
|
||||||
|
- `bgWhite`
|
||||||
|
- `bgBlackBright` (alias: `bgGray`, `bgGrey`)
|
||||||
|
- `bgRedBright`
|
||||||
|
- `bgGreenBright`
|
||||||
|
- `bgYellowBright`
|
||||||
|
- `bgBlueBright`
|
||||||
|
- `bgMagentaBright`
|
||||||
|
- `bgCyanBright`
|
||||||
|
- `bgWhiteBright`
|
||||||
|
|
||||||
|
## Advanced usage
|
||||||
|
|
||||||
|
By default, you get a map of styles, but the styles are also available as groups. They are non-enumerable so they don't show up unless you access them explicitly. This makes it easier to expose only a subset in a higher-level module.
|
||||||
|
|
||||||
|
- `style.modifier`
|
||||||
|
- `style.color`
|
||||||
|
- `style.bgColor`
|
||||||
|
|
||||||
|
###### Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log(style.color.green.open);
|
||||||
|
```
|
||||||
|
|
||||||
|
Raw escape codes (i.e. without the CSI escape prefix `\u001B[` and render mode postfix `m`) are available under `style.codes`, which returns a `Map` with the open codes as keys and close codes as values.
|
||||||
|
|
||||||
|
###### Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log(style.codes.get(36));
|
||||||
|
//=> 39
|
||||||
|
```
|
||||||
|
|
||||||
|
## [256 / 16 million (TrueColor) support](https://gist.github.com/XVilka/8346728)
|
||||||
|
|
||||||
|
`ansi-styles` uses the [`color-convert`](https://github.com/Qix-/color-convert) package to allow for converting between various colors and ANSI escapes, with support for 256 and 16 million colors.
|
||||||
|
|
||||||
|
The following color spaces from `color-convert` are supported:
|
||||||
|
|
||||||
|
- `rgb`
|
||||||
|
- `hex`
|
||||||
|
- `keyword`
|
||||||
|
- `hsl`
|
||||||
|
- `hsv`
|
||||||
|
- `hwb`
|
||||||
|
- `ansi`
|
||||||
|
- `ansi256`
|
||||||
|
|
||||||
|
To use these, call the associated conversion function with the intended output, for example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
style.color.ansi.rgb(100, 200, 15); // RGB to 16 color ansi foreground code
|
||||||
|
style.bgColor.ansi.rgb(100, 200, 15); // RGB to 16 color ansi background code
|
||||||
|
|
||||||
|
style.color.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code
|
||||||
|
style.bgColor.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code
|
||||||
|
|
||||||
|
style.color.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color foreground code
|
||||||
|
style.bgColor.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color background code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [ansi-escapes](https://github.com/sindresorhus/ansi-escapes) - ANSI escape codes for manipulating the terminal
|
||||||
|
|
||||||
|
## Maintainers
|
||||||
|
|
||||||
|
- [Sindre Sorhus](https://github.com/sindresorhus)
|
||||||
|
- [Josh Junon](https://github.com/qix-)
|
||||||
|
|
||||||
|
## For enterprise
|
||||||
|
|
||||||
|
Available as part of the Tidelift Subscription.
|
||||||
|
|
||||||
|
The maintainers of `ansi-styles` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-ansi-styles?utm_source=npm-ansi-styles&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||||
21
node_modules/array-flatten/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
43
node_modules/array-flatten/README.md
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Array Flatten
|
||||||
|
|
||||||
|
[![NPM version][npm-image]][npm-url]
|
||||||
|
[![NPM downloads][downloads-image]][downloads-url]
|
||||||
|
[![Build status][travis-image]][travis-url]
|
||||||
|
[![Test coverage][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
> Flatten an array of nested arrays into a single flat array. Accepts an optional depth.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install array-flatten --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var flatten = require('array-flatten')
|
||||||
|
|
||||||
|
flatten([1, [2, [3, [4, [5], 6], 7], 8], 9])
|
||||||
|
//=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
|
||||||
|
flatten([1, [2, [3, [4, [5], 6], 7], 8], 9], 2)
|
||||||
|
//=> [1, 2, 3, [4, [5], 6], 7, 8, 9]
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
flatten(arguments) //=> [1, 2, 3]
|
||||||
|
})(1, [2, 3])
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
[npm-image]: https://img.shields.io/npm/v/array-flatten.svg?style=flat
|
||||||
|
[npm-url]: https://npmjs.org/package/array-flatten
|
||||||
|
[downloads-image]: https://img.shields.io/npm/dm/array-flatten.svg?style=flat
|
||||||
|
[downloads-url]: https://npmjs.org/package/array-flatten
|
||||||
|
[travis-image]: https://img.shields.io/travis/blakeembrey/array-flatten.svg?style=flat
|
||||||
|
[travis-url]: https://travis-ci.org/blakeembrey/array-flatten
|
||||||
|
[coveralls-image]: https://img.shields.io/coveralls/blakeembrey/array-flatten.svg?style=flat
|
||||||
|
[coveralls-url]: https://coveralls.io/r/blakeembrey/array-flatten?branch=master
|
||||||
64
node_modules/array-flatten/array-flatten.js
generated
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose `arrayFlatten`.
|
||||||
|
*/
|
||||||
|
module.exports = arrayFlatten
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive flatten function with depth.
|
||||||
|
*
|
||||||
|
* @param {Array} array
|
||||||
|
* @param {Array} result
|
||||||
|
* @param {Number} depth
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
function flattenWithDepth (array, result, depth) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
var value = array[i]
|
||||||
|
|
||||||
|
if (depth > 0 && Array.isArray(value)) {
|
||||||
|
flattenWithDepth(value, result, depth - 1)
|
||||||
|
} else {
|
||||||
|
result.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursive flatten function. Omitting depth is slightly faster.
|
||||||
|
*
|
||||||
|
* @param {Array} array
|
||||||
|
* @param {Array} result
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
function flattenForever (array, result) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
var value = array[i]
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
flattenForever(value, result)
|
||||||
|
} else {
|
||||||
|
result.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten an array, with the ability to define a depth.
|
||||||
|
*
|
||||||
|
* @param {Array} array
|
||||||
|
* @param {Number} depth
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
function arrayFlatten (array, depth) {
|
||||||
|
if (depth == null) {
|
||||||
|
return flattenForever(array, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
return flattenWithDepth(array, [], depth)
|
||||||
|
}
|
||||||
39
node_modules/array-flatten/package.json
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "array-flatten",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"description": "Flatten an array of nested arrays into a single flat array",
|
||||||
|
"main": "array-flatten.js",
|
||||||
|
"files": [
|
||||||
|
"array-flatten.js",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "istanbul cover _mocha -- -R spec"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/blakeembrey/array-flatten.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"array",
|
||||||
|
"flatten",
|
||||||
|
"arguments",
|
||||||
|
"depth"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Blake Embrey",
|
||||||
|
"email": "hello@blakeembrey.com",
|
||||||
|
"url": "http://blakeembrey.me"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/blakeembrey/array-flatten/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/blakeembrey/array-flatten",
|
||||||
|
"devDependencies": {
|
||||||
|
"istanbul": "^0.3.13",
|
||||||
|
"mocha": "^2.2.4",
|
||||||
|
"pre-commit": "^1.0.7",
|
||||||
|
"standard": "^3.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
351
node_modules/async/CHANGELOG.md
generated
vendored
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
# v3.2.5
|
||||||
|
- Ensure `Error` objects such as `AggregateError` are propagated without modification (#1920)
|
||||||
|
|
||||||
|
# v3.2.4
|
||||||
|
- Fix a bug in `priorityQueue` where it didn't wait for the result. (#1725)
|
||||||
|
- Fix a bug where `unshiftAsync` was included in `priorityQueue`. (#1790)
|
||||||
|
|
||||||
|
# v3.2.3
|
||||||
|
- Fix bugs in comment parsing in `autoInject`. (#1767, #1780)
|
||||||
|
|
||||||
|
# v3.2.2
|
||||||
|
- Fix potential prototype pollution exploit
|
||||||
|
|
||||||
|
# v3.2.1
|
||||||
|
- Use `queueMicrotask` if available to the environment (#1761)
|
||||||
|
- Minor perf improvement in `priorityQueue` (#1727)
|
||||||
|
- More examples in documentation (#1726)
|
||||||
|
- Various doc fixes (#1708, #1712, #1717, #1740, #1739, #1749, #1756)
|
||||||
|
- Improved test coverage (#1754)
|
||||||
|
|
||||||
|
# v3.2.0
|
||||||
|
- Fix a bug in Safari related to overwriting `func.name`
|
||||||
|
- Remove built-in browserify configuration (#1653)
|
||||||
|
- Varios doc fixes (#1688, #1703, #1704)
|
||||||
|
|
||||||
|
# v3.1.1
|
||||||
|
- Allow redefining `name` property on wrapped functions.
|
||||||
|
|
||||||
|
# v3.1.0
|
||||||
|
|
||||||
|
- Added `q.pushAsync` and `q.unshiftAsync`, analagous to `q.push` and `q.unshift`, except they always do not accept a callback, and reject if processing the task errors. (#1659)
|
||||||
|
- Promises returned from `q.push` and `q.unshift` when a callback is not passed now resolve even if an error ocurred. (#1659)
|
||||||
|
- Fixed a parsing bug in `autoInject` with complicated function bodies (#1663)
|
||||||
|
- Added ES6+ configuration for Browserify bundlers (#1653)
|
||||||
|
- Various doc fixes (#1664, #1658, #1665, #1652)
|
||||||
|
|
||||||
|
# v3.0.1
|
||||||
|
|
||||||
|
## Bug fixes
|
||||||
|
- Fixed a regression where arrays passed to `queue` and `cargo` would be completely flattened. (#1645)
|
||||||
|
- Clarified Async's browser support (#1643)
|
||||||
|
|
||||||
|
|
||||||
|
# v3.0.0
|
||||||
|
|
||||||
|
The `async`/`await` release!
|
||||||
|
|
||||||
|
There are a lot of new features and subtle breaking changes in this major version, but the biggest feature is that most Async methods return a Promise if you omit the callback, meaning you can `await` them from within an `async` function.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const results = await async.mapLimit(urls, 5, async url => {
|
||||||
|
const resp = await fetch(url)
|
||||||
|
return resp.body
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
- Most Async methods return a Promise when the final callback is omitted, making them `await`-able! (#1572)
|
||||||
|
- We are now making heavy use of ES2015 features, this means we have dropped out-of-the-box support for Node 4 and earlier, and many old versions of browsers. (#1541, #1553)
|
||||||
|
- In `queue`, `priorityQueue`, `cargo` and `cargoQueue`, the "event"-style methods, like `q.drain` and `q.saturated` are now methods that register a callback, rather than properties you assign a callback to. They are now of the form `q.drain(callback)`. If you do not pass a callback a Promise will be returned for the next occurrence of the event, making them `await`-able, e.g. `await q.drain()`. (#1586, #1641)
|
||||||
|
- Calling `callback(false)` will cancel an async method, preventing further iteration and callback calls. This is useful for preventing memory leaks when you break out of an async flow by calling an outer callback. (#1064, #1542)
|
||||||
|
- `during` and `doDuring` have been removed, and instead `whilst`, `doWhilst`, `until` and `doUntil` now have asynchronous `test` functions. (#850, #1557)
|
||||||
|
- `limits` of less than 1 now cause an error to be thrown in queues and collection methods. (#1249, #1552)
|
||||||
|
- `memoize` no longer memoizes errors (#1465, #1466)
|
||||||
|
- `applyEach`/`applyEachSeries` have a simpler interface, to make them more easily type-able. It always returns a function that takes in a single callback argument. If that callback is omitted, a promise is returned, making it awaitable. (#1228, #1640)
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
- Async generators are now supported in all the Collection methods. (#1560)
|
||||||
|
- Added `cargoQueue`, a queue with both `concurrency` and `payload` size parameters. (#1567)
|
||||||
|
- Queue objects returned from `queue` now have a `Symbol.iterator` method, meaning they can be iterated over to inspect the current list of items in the queue. (#1459, #1556)
|
||||||
|
- A ESM-flavored `async.mjs` is included in the `async` package. This is described in the `package.json` `"module"` field, meaning it should be automatically used by Webpack and other compatible bundlers.
|
||||||
|
|
||||||
|
## Bug fixes
|
||||||
|
- Better handle arbitrary error objects in `asyncify` (#1568, #1569)
|
||||||
|
|
||||||
|
## Other
|
||||||
|
- Removed Lodash as a dependency (#1283, #1528)
|
||||||
|
- Miscellaneous docs fixes (#1393, #1501, #1540, #1543, #1558, #1563, #1564, #1579, #1581)
|
||||||
|
- Miscellaneous test fixes (#1538)
|
||||||
|
|
||||||
|
-------
|
||||||
|
|
||||||
|
# v2.6.1
|
||||||
|
- Updated lodash to prevent `npm audit` warnings. (#1532, #1533)
|
||||||
|
- Made `async-es` more optimized for webpack users (#1517)
|
||||||
|
- Fixed a stack overflow with large collections and a synchronous iterator (#1514)
|
||||||
|
- Various small fixes/chores (#1505, #1511, #1527, #1530)
|
||||||
|
|
||||||
|
# v2.6.0
|
||||||
|
- Added missing aliases for many methods. Previously, you could not (e.g.) `require('async/find')` or use `async.anyLimit`. (#1483)
|
||||||
|
- Improved `queue` performance. (#1448, #1454)
|
||||||
|
- Add missing sourcemap (#1452, #1453)
|
||||||
|
- Various doc updates (#1448, #1471, #1483)
|
||||||
|
|
||||||
|
# v2.5.0
|
||||||
|
- Added `concatLimit`, the `Limit` equivalent of [`concat`](https://caolan.github.io/async/docs.html#concat) ([#1426](https://github.com/caolan/async/issues/1426), [#1430](https://github.com/caolan/async/pull/1430))
|
||||||
|
- `concat` improvements: it now preserves order, handles falsy values and the `iteratee` callback takes a variable number of arguments ([#1437](https://github.com/caolan/async/issues/1437), [#1436](https://github.com/caolan/async/pull/1436))
|
||||||
|
- Fixed an issue in `queue` where there was a size discrepancy between `workersList().length` and `running()` ([#1428](https://github.com/caolan/async/issues/1428), [#1429](https://github.com/caolan/async/pull/1429))
|
||||||
|
- Various doc fixes ([#1422](https://github.com/caolan/async/issues/1422), [#1424](https://github.com/caolan/async/pull/1424))
|
||||||
|
|
||||||
|
# v2.4.1
|
||||||
|
- Fixed a bug preventing functions wrapped with `timeout()` from being re-used. ([#1418](https://github.com/caolan/async/issues/1418), [#1419](https://github.com/caolan/async/issues/1419))
|
||||||
|
|
||||||
|
# v2.4.0
|
||||||
|
- Added `tryEach`, for running async functions in parallel, where you only expect one to succeed. ([#1365](https://github.com/caolan/async/issues/1365), [#687](https://github.com/caolan/async/issues/687))
|
||||||
|
- Improved performance, most notably in `parallel` and `waterfall` ([#1395](https://github.com/caolan/async/issues/1395))
|
||||||
|
- Added `queue.remove()`, for removing items in a `queue` ([#1397](https://github.com/caolan/async/issues/1397), [#1391](https://github.com/caolan/async/issues/1391))
|
||||||
|
- Fixed using `eval`, preventing Async from running in pages with Content Security Policy ([#1404](https://github.com/caolan/async/issues/1404), [#1403](https://github.com/caolan/async/issues/1403))
|
||||||
|
- Fixed errors thrown in an `asyncify`ed function's callback being caught by the underlying Promise ([#1408](https://github.com/caolan/async/issues/1408))
|
||||||
|
- Fixed timing of `queue.empty()` ([#1367](https://github.com/caolan/async/issues/1367))
|
||||||
|
- Various doc fixes ([#1314](https://github.com/caolan/async/issues/1314), [#1394](https://github.com/caolan/async/issues/1394), [#1412](https://github.com/caolan/async/issues/1412))
|
||||||
|
|
||||||
|
# v2.3.0
|
||||||
|
- Added support for ES2017 `async` functions. Wherever you can pass a Node-style/CPS function that uses a callback, you can also pass an `async` function. Previously, you had to wrap `async` functions with `asyncify`. The caveat is that it will only work if `async` functions are supported natively in your environment, transpiled implementations can't be detected. ([#1386](https://github.com/caolan/async/issues/1386), [#1390](https://github.com/caolan/async/issues/1390))
|
||||||
|
- Small doc fix ([#1392](https://github.com/caolan/async/issues/1392))
|
||||||
|
|
||||||
|
# v2.2.0
|
||||||
|
- Added `groupBy`, and the `Series`/`Limit` equivalents, analogous to [`_.groupBy`](http://lodash.com/docs#groupBy) ([#1364](https://github.com/caolan/async/issues/1364))
|
||||||
|
- Fixed `transform` bug when `callback` was not passed ([#1381](https://github.com/caolan/async/issues/1381))
|
||||||
|
- Added note about `reflect` to `parallel` docs ([#1385](https://github.com/caolan/async/issues/1385))
|
||||||
|
|
||||||
|
# v2.1.5
|
||||||
|
- Fix `auto` bug when function names collided with Array.prototype ([#1358](https://github.com/caolan/async/issues/1358))
|
||||||
|
- Improve some error messages ([#1349](https://github.com/caolan/async/issues/1349))
|
||||||
|
- Avoid stack overflow case in queue
|
||||||
|
- Fixed an issue in `some`, `every` and `find` where processing would continue after the result was determined.
|
||||||
|
- Cleanup implementations of `some`, `every` and `find`
|
||||||
|
|
||||||
|
# v2.1.3
|
||||||
|
- Make bundle size smaller
|
||||||
|
- Create optimized hotpath for `filter` in array case.
|
||||||
|
|
||||||
|
# v2.1.2
|
||||||
|
- Fixed a stackoverflow bug with `detect`, `some`, `every` on large inputs ([#1293](https://github.com/caolan/async/issues/1293)).
|
||||||
|
|
||||||
|
# v2.1.0
|
||||||
|
|
||||||
|
- `retry` and `retryable` now support an optional `errorFilter` function that determines if the `task` should retry on the error ([#1256](https://github.com/caolan/async/issues/1256), [#1261](https://github.com/caolan/async/issues/1261))
|
||||||
|
- Optimized array iteration in `race`, `cargo`, `queue`, and `priorityQueue` ([#1253](https://github.com/caolan/async/issues/1253))
|
||||||
|
- Added alias documentation to doc site ([#1251](https://github.com/caolan/async/issues/1251), [#1254](https://github.com/caolan/async/issues/1254))
|
||||||
|
- Added [BootStrap scrollspy](http://getbootstrap.com/javascript/#scrollspy) to docs to highlight in the sidebar the current method being viewed ([#1289](https://github.com/caolan/async/issues/1289), [#1300](https://github.com/caolan/async/issues/1300))
|
||||||
|
- Various minor doc fixes ([#1263](https://github.com/caolan/async/issues/1263), [#1264](https://github.com/caolan/async/issues/1264), [#1271](https://github.com/caolan/async/issues/1271), [#1278](https://github.com/caolan/async/issues/1278), [#1280](https://github.com/caolan/async/issues/1280), [#1282](https://github.com/caolan/async/issues/1282), [#1302](https://github.com/caolan/async/issues/1302))
|
||||||
|
|
||||||
|
# v2.0.1
|
||||||
|
|
||||||
|
- Significantly optimized all iteration based collection methods such as `each`, `map`, `filter`, etc ([#1245](https://github.com/caolan/async/issues/1245), [#1246](https://github.com/caolan/async/issues/1246), [#1247](https://github.com/caolan/async/issues/1247)).
|
||||||
|
|
||||||
|
# v2.0.0
|
||||||
|
|
||||||
|
Lots of changes here!
|
||||||
|
|
||||||
|
First and foremost, we have a slick new [site for docs](https://caolan.github.io/async/). Special thanks to [**@hargasinski**](https://github.com/hargasinski) for his work converting our old docs to `jsdoc` format and implementing the new website. Also huge ups to [**@ivanseidel**](https://github.com/ivanseidel) for designing our new logo. It was a long process for both of these tasks, but I think these changes turned out extraordinary well.
|
||||||
|
|
||||||
|
The biggest feature is modularization. You can now `require("async/series")` to only require the `series` function. Every Async library function is available this way. You still can `require("async")` to require the entire library, like you could do before.
|
||||||
|
|
||||||
|
We also provide Async as a collection of ES2015 modules. You can now `import {each} from 'async-es'` or `import waterfall from 'async-es/waterfall'`. If you are using only a few Async functions, and are using a ES bundler such as Rollup, this can significantly lower your build size.
|
||||||
|
|
||||||
|
Major thanks to [**@Kikobeats**](github.com/Kikobeats), [**@aearly**](github.com/aearly) and [**@megawac**](github.com/megawac) for doing the majority of the modularization work, as well as [**@jdalton**](github.com/jdalton) and [**@Rich-Harris**](github.com/Rich-Harris) for advisory work on the general modularization strategy.
|
||||||
|
|
||||||
|
Another one of the general themes of the 2.0 release is standardization of what an "async" function is. We are now more strictly following the node-style continuation passing style. That is, an async function is a function that:
|
||||||
|
|
||||||
|
1. Takes a variable number of arguments
|
||||||
|
2. The last argument is always a callback
|
||||||
|
3. The callback can accept any number of arguments
|
||||||
|
4. The first argument passed to the callback will be treated as an error result, if the argument is truthy
|
||||||
|
5. Any number of result arguments can be passed after the "error" argument
|
||||||
|
6. The callback is called once and exactly once, either on the same tick or later tick of the JavaScript event loop.
|
||||||
|
|
||||||
|
There were several cases where Async accepted some functions that did not strictly have these properties, most notably `auto`, `every`, `some`, `filter`, `reject` and `detect`.
|
||||||
|
|
||||||
|
Another theme is performance. We have eliminated internal deferrals in all cases where they make sense. For example, in `waterfall` and `auto`, there was a `setImmediate` between each task -- these deferrals have been removed. A `setImmediate` call can add up to 1ms of delay. This might not seem like a lot, but it can add up if you are using many Async functions in the course of processing a HTTP request, for example. Nearly all asynchronous functions that do I/O already have some sort of deferral built in, so the extra deferral is unnecessary. The trade-off of this change is removing our built-in stack-overflow defense. Many synchronous callback calls in series can quickly overflow the JS call stack. If you do have a function that is sometimes synchronous (calling its callback on the same tick), and are running into stack overflows, wrap it with `async.ensureAsync()`.
|
||||||
|
|
||||||
|
Another big performance win has been re-implementing `queue`, `cargo`, and `priorityQueue` with [doubly linked lists](https://en.wikipedia.org/wiki/Doubly_linked_list) instead of arrays. This has lead to queues being an order of [magnitude faster on large sets of tasks](https://github.com/caolan/async/pull/1205).
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
- Async is now modularized. Individual functions can be `require()`d from the main package. (`require('async/auto')`) ([#984](https://github.com/caolan/async/issues/984), [#996](https://github.com/caolan/async/issues/996))
|
||||||
|
- Async is also available as a collection of ES2015 modules in the new `async-es` package. (`import {forEachSeries} from 'async-es'`) ([#984](https://github.com/caolan/async/issues/984), [#996](https://github.com/caolan/async/issues/996))
|
||||||
|
- Added `race`, analogous to `Promise.race()`. It will run an array of async tasks in parallel and will call its callback with the result of the first task to respond. ([#568](https://github.com/caolan/async/issues/568), [#1038](https://github.com/caolan/async/issues/1038))
|
||||||
|
- Collection methods now accept ES2015 iterators. Maps, Sets, and anything that implements the iterator spec can now be passed directly to `each`, `map`, `parallel`, etc.. ([#579](https://github.com/caolan/async/issues/579), [#839](https://github.com/caolan/async/issues/839), [#1074](https://github.com/caolan/async/issues/1074))
|
||||||
|
- Added `mapValues`, for mapping over the properties of an object and returning an object with the same keys. ([#1157](https://github.com/caolan/async/issues/1157), [#1177](https://github.com/caolan/async/issues/1177))
|
||||||
|
- Added `timeout`, a wrapper for an async function that will make the task time-out after the specified time. ([#1007](https://github.com/caolan/async/issues/1007), [#1027](https://github.com/caolan/async/issues/1027))
|
||||||
|
- Added `reflect` and `reflectAll`, analagous to [`Promise.reflect()`](http://bluebirdjs.com/docs/api/reflect.html), a wrapper for async tasks that always succeeds, by gathering results and errors into an object. ([#942](https://github.com/caolan/async/issues/942), [#1012](https://github.com/caolan/async/issues/1012), [#1095](https://github.com/caolan/async/issues/1095))
|
||||||
|
- `constant` supports dynamic arguments -- it will now always use its last argument as the callback. ([#1016](https://github.com/caolan/async/issues/1016), [#1052](https://github.com/caolan/async/issues/1052))
|
||||||
|
- `setImmediate` and `nextTick` now support arguments to partially apply to the deferred function, like the node-native versions do. ([#940](https://github.com/caolan/async/issues/940), [#1053](https://github.com/caolan/async/issues/1053))
|
||||||
|
- `auto` now supports resolving cyclic dependencies using [Kahn's algorithm](https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm) ([#1140](https://github.com/caolan/async/issues/1140)).
|
||||||
|
- Added `autoInject`, a relative of `auto` that automatically spreads a task's dependencies as arguments to the task function. ([#608](https://github.com/caolan/async/issues/608), [#1055](https://github.com/caolan/async/issues/1055), [#1099](https://github.com/caolan/async/issues/1099), [#1100](https://github.com/caolan/async/issues/1100))
|
||||||
|
- You can now limit the concurrency of `auto` tasks. ([#635](https://github.com/caolan/async/issues/635), [#637](https://github.com/caolan/async/issues/637))
|
||||||
|
- Added `retryable`, a relative of `retry` that wraps an async function, making it retry when called. ([#1058](https://github.com/caolan/async/issues/1058))
|
||||||
|
- `retry` now supports specifying a function that determines the next time interval, useful for exponential backoff, logging and other retry strategies. ([#1161](https://github.com/caolan/async/issues/1161))
|
||||||
|
- `retry` will now pass all of the arguments the task function was resolved with to the callback ([#1231](https://github.com/caolan/async/issues/1231)).
|
||||||
|
- Added `q.unsaturated` -- callback called when a `queue`'s number of running workers falls below a threshold. ([#868](https://github.com/caolan/async/issues/868), [#1030](https://github.com/caolan/async/issues/1030), [#1033](https://github.com/caolan/async/issues/1033), [#1034](https://github.com/caolan/async/issues/1034))
|
||||||
|
- Added `q.error` -- a callback called whenever a `queue` task calls its callback with an error. ([#1170](https://github.com/caolan/async/issues/1170))
|
||||||
|
- `applyEach` and `applyEachSeries` now pass results to the final callback. ([#1088](https://github.com/caolan/async/issues/1088))
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
- Calling a callback more than once is considered an error, and an error will be thrown. This had an explicit breaking change in `waterfall`. If you were relying on this behavior, you should more accurately represent your control flow as an event emitter or stream. ([#814](https://github.com/caolan/async/issues/814), [#815](https://github.com/caolan/async/issues/815), [#1048](https://github.com/caolan/async/issues/1048), [#1050](https://github.com/caolan/async/issues/1050))
|
||||||
|
- `auto` task functions now always take the callback as the last argument. If a task has dependencies, the `results` object will be passed as the first argument. To migrate old task functions, wrap them with [`_.flip`](https://lodash.com/docs#flip) ([#1036](https://github.com/caolan/async/issues/1036), [#1042](https://github.com/caolan/async/issues/1042))
|
||||||
|
- Internal `setImmediate` calls have been refactored away. This may make existing flows vulnerable to stack overflows if you use many synchronous functions in series. Use `ensureAsync` to work around this. ([#696](https://github.com/caolan/async/issues/696), [#704](https://github.com/caolan/async/issues/704), [#1049](https://github.com/caolan/async/issues/1049), [#1050](https://github.com/caolan/async/issues/1050))
|
||||||
|
- `map` used to return an object when iterating over an object. `map` now always returns an array, like in other libraries. The previous object behavior has been split out into `mapValues`. ([#1157](https://github.com/caolan/async/issues/1157), [#1177](https://github.com/caolan/async/issues/1177))
|
||||||
|
- `filter`, `reject`, `some`, `every`, `detect` and their families like `{METHOD}Series` and `{METHOD}Limit` now expect an error as the first callback argument, rather than just a simple boolean. Pass `null` as the first argument, or use `fs.access` instead of `fs.exists`. ([#118](https://github.com/caolan/async/issues/118), [#774](https://github.com/caolan/async/issues/774), [#1028](https://github.com/caolan/async/issues/1028), [#1041](https://github.com/caolan/async/issues/1041))
|
||||||
|
- `{METHOD}` and `{METHOD}Series` are now implemented in terms of `{METHOD}Limit`. This is a major internal simplification, and is not expected to cause many problems, but it does subtly affect how functions execute internally. ([#778](https://github.com/caolan/async/issues/778), [#847](https://github.com/caolan/async/issues/847))
|
||||||
|
- `retry`'s callback is now optional. Previously, omitting the callback would partially apply the function, meaning it could be passed directly as a task to `series` or `auto`. The partially applied "control-flow" behavior has been separated out into `retryable`. ([#1054](https://github.com/caolan/async/issues/1054), [#1058](https://github.com/caolan/async/issues/1058))
|
||||||
|
- The test function for `whilst`, `until`, and `during` used to be passed non-error args from the iteratee function's callback, but this led to weirdness where the first call of the test function would be passed no args. We have made it so the test function is never passed extra arguments, and only the `doWhilst`, `doUntil`, and `doDuring` functions pass iteratee callback arguments to the test function ([#1217](https://github.com/caolan/async/issues/1217), [#1224](https://github.com/caolan/async/issues/1224))
|
||||||
|
- The `q.tasks` array has been renamed `q._tasks` and is now implemented as a doubly linked list (DLL). Any code that used to interact with this array will need to be updated to either use the provided helpers or support DLLs ([#1205](https://github.com/caolan/async/issues/1205)).
|
||||||
|
- The timing of the `q.saturated()` callback in a `queue` has been modified to better reflect when tasks pushed to the queue will start queueing. ([#724](https://github.com/caolan/async/issues/724), [#1078](https://github.com/caolan/async/issues/1078))
|
||||||
|
- Removed `iterator` method in favour of [ES2015 iterator protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators ) which natively supports arrays ([#1237](https://github.com/caolan/async/issues/1237))
|
||||||
|
- Dropped support for Component, Jam, SPM, and Volo ([#1175](https://github.com/caolan/async/issues/1175), #[#176](https://github.com/caolan/async/issues/176))
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
- Improved handling of no dependency cases in `auto` & `autoInject` ([#1147](https://github.com/caolan/async/issues/1147)).
|
||||||
|
- Fixed a bug where the callback generated by `asyncify` with `Promises` could resolve twice ([#1197](https://github.com/caolan/async/issues/1197)).
|
||||||
|
- Fixed several documented optional callbacks not actually being optional ([#1223](https://github.com/caolan/async/issues/1223)).
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
- Added `someSeries` and `everySeries` for symmetry, as well as a complete set of `any`/`anyLimit`/`anySeries` and `all`/`/allLmit`/`allSeries` aliases.
|
||||||
|
- Added `find` as an alias for `detect. (as well as `findLimit` and `findSeries`).
|
||||||
|
- Various doc fixes ([#1005](https://github.com/caolan/async/issues/1005), [#1008](https://github.com/caolan/async/issues/1008), [#1010](https://github.com/caolan/async/issues/1010), [#1015](https://github.com/caolan/async/issues/1015), [#1021](https://github.com/caolan/async/issues/1021), [#1037](https://github.com/caolan/async/issues/1037), [#1039](https://github.com/caolan/async/issues/1039), [#1051](https://github.com/caolan/async/issues/1051), [#1102](https://github.com/caolan/async/issues/1102), [#1107](https://github.com/caolan/async/issues/1107), [#1121](https://github.com/caolan/async/issues/1121), [#1123](https://github.com/caolan/async/issues/1123), [#1129](https://github.com/caolan/async/issues/1129), [#1135](https://github.com/caolan/async/issues/1135), [#1138](https://github.com/caolan/async/issues/1138), [#1141](https://github.com/caolan/async/issues/1141), [#1153](https://github.com/caolan/async/issues/1153), [#1216](https://github.com/caolan/async/issues/1216), [#1217](https://github.com/caolan/async/issues/1217), [#1232](https://github.com/caolan/async/issues/1232), [#1233](https://github.com/caolan/async/issues/1233), [#1236](https://github.com/caolan/async/issues/1236), [#1238](https://github.com/caolan/async/issues/1238))
|
||||||
|
|
||||||
|
Thank you [**@aearly**](github.com/aearly) and [**@megawac**](github.com/megawac) for taking the lead on version 2 of async.
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
# v1.5.2
|
||||||
|
- Allow using `"constructor"` as an argument in `memoize` ([#998](https://github.com/caolan/async/issues/998))
|
||||||
|
- Give a better error messsage when `auto` dependency checking fails ([#994](https://github.com/caolan/async/issues/994))
|
||||||
|
- Various doc updates ([#936](https://github.com/caolan/async/issues/936), [#956](https://github.com/caolan/async/issues/956), [#979](https://github.com/caolan/async/issues/979), [#1002](https://github.com/caolan/async/issues/1002))
|
||||||
|
|
||||||
|
# v1.5.1
|
||||||
|
- Fix issue with `pause` in `queue` with concurrency enabled ([#946](https://github.com/caolan/async/issues/946))
|
||||||
|
- `while` and `until` now pass the final result to callback ([#963](https://github.com/caolan/async/issues/963))
|
||||||
|
- `auto` will properly handle concurrency when there is no callback ([#966](https://github.com/caolan/async/issues/966))
|
||||||
|
- `auto` will no. properly stop execution when an error occurs ([#988](https://github.com/caolan/async/issues/988), [#993](https://github.com/caolan/async/issues/993))
|
||||||
|
- Various doc fixes ([#971](https://github.com/caolan/async/issues/971), [#980](https://github.com/caolan/async/issues/980))
|
||||||
|
|
||||||
|
# v1.5.0
|
||||||
|
|
||||||
|
- Added `transform`, analogous to [`_.transform`](http://lodash.com/docs#transform) ([#892](https://github.com/caolan/async/issues/892))
|
||||||
|
- `map` now returns an object when an object is passed in, rather than array with non-numeric keys. `map` will begin always returning an array with numeric indexes in the next major release. ([#873](https://github.com/caolan/async/issues/873))
|
||||||
|
- `auto` now accepts an optional `concurrency` argument to limit the number o. running tasks ([#637](https://github.com/caolan/async/issues/637))
|
||||||
|
- Added `queue#workersList()`, to retrieve the lis. of currently running tasks. ([#891](https://github.com/caolan/async/issues/891))
|
||||||
|
- Various code simplifications ([#896](https://github.com/caolan/async/issues/896), [#904](https://github.com/caolan/async/issues/904))
|
||||||
|
- Various doc fixes :scroll: ([#890](https://github.com/caolan/async/issues/890), [#894](https://github.com/caolan/async/issues/894), [#903](https://github.com/caolan/async/issues/903), [#905](https://github.com/caolan/async/issues/905), [#912](https://github.com/caolan/async/issues/912))
|
||||||
|
|
||||||
|
# v1.4.2
|
||||||
|
|
||||||
|
- Ensure coverage files don't get published on npm ([#879](https://github.com/caolan/async/issues/879))
|
||||||
|
|
||||||
|
# v1.4.1
|
||||||
|
|
||||||
|
- Add in overlooked `detectLimit` method ([#866](https://github.com/caolan/async/issues/866))
|
||||||
|
- Removed unnecessary files from npm releases ([#861](https://github.com/caolan/async/issues/861))
|
||||||
|
- Removed usage of a reserved word to prevent :boom: in older environments ([#870](https://github.com/caolan/async/issues/870))
|
||||||
|
|
||||||
|
# v1.4.0
|
||||||
|
|
||||||
|
- `asyncify` now supports promises ([#840](https://github.com/caolan/async/issues/840))
|
||||||
|
- Added `Limit` versions of `filter` and `reject` ([#836](https://github.com/caolan/async/issues/836))
|
||||||
|
- Add `Limit` versions of `detect`, `some` and `every` ([#828](https://github.com/caolan/async/issues/828), [#829](https://github.com/caolan/async/issues/829))
|
||||||
|
- `some`, `every` and `detect` now short circuit early ([#828](https://github.com/caolan/async/issues/828), [#829](https://github.com/caolan/async/issues/829))
|
||||||
|
- Improve detection of the global object ([#804](https://github.com/caolan/async/issues/804)), enabling use in WebWorkers
|
||||||
|
- `whilst` now called with arguments from iterator ([#823](https://github.com/caolan/async/issues/823))
|
||||||
|
- `during` now gets called with arguments from iterator ([#824](https://github.com/caolan/async/issues/824))
|
||||||
|
- Code simplifications and optimizations aplenty ([diff](https://github.com/caolan/async/compare/v1.3.0...v1.4.0))
|
||||||
|
|
||||||
|
|
||||||
|
# v1.3.0
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
- Added `constant`
|
||||||
|
- Added `asyncify`/`wrapSync` for making sync functions work with callbacks. ([#671](https://github.com/caolan/async/issues/671), [#806](https://github.com/caolan/async/issues/806))
|
||||||
|
- Added `during` and `doDuring`, which are like `whilst` with an async truth test. ([#800](https://github.com/caolan/async/issues/800))
|
||||||
|
- `retry` now accepts an `interval` parameter to specify a delay between retries. ([#793](https://github.com/caolan/async/issues/793))
|
||||||
|
- `async` should work better in Web Workers due to better `root` detection ([#804](https://github.com/caolan/async/issues/804))
|
||||||
|
- Callbacks are now optional in `whilst`, `doWhilst`, `until`, and `doUntil` ([#642](https://github.com/caolan/async/issues/642))
|
||||||
|
- Various internal updates ([#786](https://github.com/caolan/async/issues/786), [#801](https://github.com/caolan/async/issues/801), [#802](https://github.com/caolan/async/issues/802), [#803](https://github.com/caolan/async/issues/803))
|
||||||
|
- Various doc fixes ([#790](https://github.com/caolan/async/issues/790), [#794](https://github.com/caolan/async/issues/794))
|
||||||
|
|
||||||
|
Bug Fixes:
|
||||||
|
- `cargo` now exposes the `payload` size, and `cargo.payload` can be changed on the fly after the `cargo` is created. ([#740](https://github.com/caolan/async/issues/740), [#744](https://github.com/caolan/async/issues/744), [#783](https://github.com/caolan/async/issues/783))
|
||||||
|
|
||||||
|
|
||||||
|
# v1.2.1
|
||||||
|
|
||||||
|
Bug Fix:
|
||||||
|
|
||||||
|
- Small regression with synchronous iterator behavior in `eachSeries` with a 1-element array. Before 1.1.0, `eachSeries`'s callback was called on the same tick, which this patch restores. In 2.0.0, it will be called on the next tick. ([#782](https://github.com/caolan/async/issues/782))
|
||||||
|
|
||||||
|
|
||||||
|
# v1.2.0
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Added `timesLimit` ([#743](https://github.com/caolan/async/issues/743))
|
||||||
|
- `concurrency` can be changed after initialization in `queue` by setting `q.concurrency`. The new concurrency will be reflected the next time a task is processed. ([#747](https://github.com/caolan/async/issues/747), [#772](https://github.com/caolan/async/issues/772))
|
||||||
|
|
||||||
|
Bug Fixes:
|
||||||
|
|
||||||
|
- Fixed a regression in `each` and family with empty arrays that have additional properties. ([#775](https://github.com/caolan/async/issues/775), [#777](https://github.com/caolan/async/issues/777))
|
||||||
|
|
||||||
|
|
||||||
|
# v1.1.1
|
||||||
|
|
||||||
|
Bug Fix:
|
||||||
|
|
||||||
|
- Small regression with synchronous iterator behavior in `eachSeries` with a 1-element array. Before 1.1.0, `eachSeries`'s callback was called on the same tick, which this patch restores. In 2.0.0, it will be called on the next tick. ([#782](https://github.com/caolan/async/issues/782))
|
||||||
|
|
||||||
|
|
||||||
|
# v1.1.0
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- `cargo` now supports all of the same methods and event callbacks as `queue`.
|
||||||
|
- Added `ensureAsync` - A wrapper that ensures an async function calls its callback on a later tick. ([#769](https://github.com/caolan/async/issues/769))
|
||||||
|
- Optimized `map`, `eachOf`, and `waterfall` families of functions
|
||||||
|
- Passing a `null` or `undefined` array to `map`, `each`, `parallel` and families will be treated as an empty array ([#667](https://github.com/caolan/async/issues/667)).
|
||||||
|
- The callback is now optional for the composed results of `compose` and `seq`. ([#618](https://github.com/caolan/async/issues/618))
|
||||||
|
- Reduced file size by 4kb, (minified version by 1kb)
|
||||||
|
- Added code coverage through `nyc` and `coveralls` ([#768](https://github.com/caolan/async/issues/768))
|
||||||
|
|
||||||
|
Bug Fixes:
|
||||||
|
|
||||||
|
- `forever` will no longer stack overflow with a synchronous iterator ([#622](https://github.com/caolan/async/issues/622))
|
||||||
|
- `eachLimit` and other limit functions will stop iterating once an error occurs ([#754](https://github.com/caolan/async/issues/754))
|
||||||
|
- Always pass `null` in callbacks when there is no error ([#439](https://github.com/caolan/async/issues/439))
|
||||||
|
- Ensure proper conditions when calling `drain()` after pushing an empty data set to a queue ([#668](https://github.com/caolan/async/issues/668))
|
||||||
|
- `each` and family will properly handle an empty array ([#578](https://github.com/caolan/async/issues/578))
|
||||||
|
- `eachSeries` and family will finish if the underlying array is modified during execution ([#557](https://github.com/caolan/async/issues/557))
|
||||||
|
- `queue` will throw if a non-function is passed to `q.push()` ([#593](https://github.com/caolan/async/issues/593))
|
||||||
|
- Doc fixes ([#629](https://github.com/caolan/async/issues/629), [#766](https://github.com/caolan/async/issues/766))
|
||||||
|
|
||||||
|
|
||||||
|
# v1.0.0
|
||||||
|
|
||||||
|
No known breaking changes, we are simply complying with semver from here on out.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Start using a changelog!
|
||||||
|
- Add `forEachOf` for iterating over Objects (or to iterate Arrays with indexes available) ([#168](https://github.com/caolan/async/issues/168) [#704](https://github.com/caolan/async/issues/704) [#321](https://github.com/caolan/async/issues/321))
|
||||||
|
- Detect deadlocks in `auto` ([#663](https://github.com/caolan/async/issues/663))
|
||||||
|
- Better support for require.js ([#527](https://github.com/caolan/async/issues/527))
|
||||||
|
- Throw if queue created with concurrency `0` ([#714](https://github.com/caolan/async/issues/714))
|
||||||
|
- Fix unneeded iteration in `queue.resume()` ([#758](https://github.com/caolan/async/issues/758))
|
||||||
|
- Guard against timer mocking overriding `setImmediate` ([#609](https://github.com/caolan/async/issues/609) [#611](https://github.com/caolan/async/issues/611))
|
||||||
|
- Miscellaneous doc fixes ([#542](https://github.com/caolan/async/issues/542) [#596](https://github.com/caolan/async/issues/596) [#615](https://github.com/caolan/async/issues/615) [#628](https://github.com/caolan/async/issues/628) [#631](https://github.com/caolan/async/issues/631) [#690](https://github.com/caolan/async/issues/690) [#729](https://github.com/caolan/async/issues/729))
|
||||||
|
- Use single noop function internally ([#546](https://github.com/caolan/async/issues/546))
|
||||||
|
- Optimize internal `_each`, `_map` and `_keys` functions.
|
||||||
19
node_modules/async/LICENSE
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2010-2018 Caolan McMahon
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
59
node_modules/async/README.md
generated
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
[](https://www.npmjs.com/package/async)
|
||||||
|
[](https://coveralls.io/r/caolan/async?branch=master)
|
||||||
|
[](https://gitter.im/caolan/async?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
[](https://www.jsdelivr.com/package/npm/async)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|Linux|Windows|MacOS|
|
||||||
|
|-|-|-|
|
||||||
|
|[](https://dev.azure.com/caolanmcmahon/async/_build/latest?definitionId=1&branchName=master) | [](https://dev.azure.com/caolanmcmahon/async/_build/latest?definitionId=1&branchName=master) | [](https://dev.azure.com/caolanmcmahon/async/_build/latest?definitionId=1&branchName=master)| -->
|
||||||
|
|
||||||
|
Async is a utility module which provides straight-forward, powerful functions for working with [asynchronous JavaScript](http://caolan.github.io/async/v3/global.html). Although originally designed for use with [Node.js](https://nodejs.org/) and installable via `npm i async`, it can also be used directly in the browser. An ESM/MJS version is included in the main `async` package that should automatically be used with compatible bundlers such as Webpack and Rollup.
|
||||||
|
|
||||||
|
A pure ESM version of Async is available as [`async-es`](https://www.npmjs.com/package/async-es).
|
||||||
|
|
||||||
|
For Documentation, visit <https://caolan.github.io/async/>
|
||||||
|
|
||||||
|
*For Async v1.5.x documentation, go [HERE](https://github.com/caolan/async/blob/v1.5.2/README.md)*
|
||||||
|
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// for use with Node-style callbacks...
|
||||||
|
var async = require("async");
|
||||||
|
|
||||||
|
var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"};
|
||||||
|
var configs = {};
|
||||||
|
|
||||||
|
async.forEachOf(obj, (value, key, callback) => {
|
||||||
|
fs.readFile(__dirname + value, "utf8", (err, data) => {
|
||||||
|
if (err) return callback(err);
|
||||||
|
try {
|
||||||
|
configs[key] = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
return callback(e);
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}, err => {
|
||||||
|
if (err) console.error(err.message);
|
||||||
|
// configs is now a map of JSON data
|
||||||
|
doSomethingWith(configs);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var async = require("async");
|
||||||
|
|
||||||
|
// ...or ES2017 async functions
|
||||||
|
async.mapLimit(urls, 5, async function(url) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
return response.body
|
||||||
|
}, (err, results) => {
|
||||||
|
if (err) throw err
|
||||||
|
// results is now an array of the response bodies
|
||||||
|
console.log(results)
|
||||||
|
})
|
||||||
|
```
|
||||||
119
node_modules/async/all.js
generated
vendored
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _createTester = require('./internal/createTester.js');
|
||||||
|
|
||||||
|
var _createTester2 = _interopRequireDefault(_createTester);
|
||||||
|
|
||||||
|
var _eachOf = require('./eachOf.js');
|
||||||
|
|
||||||
|
var _eachOf2 = _interopRequireDefault(_eachOf);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if every element in `coll` satisfies an async test. If any
|
||||||
|
* iteratee call returns `false`, the main `callback` is immediately called.
|
||||||
|
*
|
||||||
|
* @name every
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @alias all
|
||||||
|
* @category Collection
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {AsyncFunction} iteratee - An async truth test to apply to each item
|
||||||
|
* in the collection in parallel.
|
||||||
|
* The iteratee must complete with a boolean result value.
|
||||||
|
* Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called after all the
|
||||||
|
* `iteratee` functions have finished. Result will be either `true` or `false`
|
||||||
|
* depending on the values of the async tests. Invoked with (err, result).
|
||||||
|
* @returns {Promise} a promise, if no callback provided
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* // dir1 is a directory that contains file1.txt, file2.txt
|
||||||
|
* // dir2 is a directory that contains file3.txt, file4.txt
|
||||||
|
* // dir3 is a directory that contains file5.txt
|
||||||
|
* // dir4 does not exist
|
||||||
|
*
|
||||||
|
* const fileList = ['dir1/file1.txt','dir2/file3.txt','dir3/file5.txt'];
|
||||||
|
* const withMissingFileList = ['file1.txt','file2.txt','file4.txt'];
|
||||||
|
*
|
||||||
|
* // asynchronous function that checks if a file exists
|
||||||
|
* function fileExists(file, callback) {
|
||||||
|
* fs.access(file, fs.constants.F_OK, (err) => {
|
||||||
|
* callback(null, !err);
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Using callbacks
|
||||||
|
* async.every(fileList, fileExists, function(err, result) {
|
||||||
|
* console.log(result);
|
||||||
|
* // true
|
||||||
|
* // result is true since every file exists
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* async.every(withMissingFileList, fileExists, function(err, result) {
|
||||||
|
* console.log(result);
|
||||||
|
* // false
|
||||||
|
* // result is false since NOT every file exists
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Using Promises
|
||||||
|
* async.every(fileList, fileExists)
|
||||||
|
* .then( result => {
|
||||||
|
* console.log(result);
|
||||||
|
* // true
|
||||||
|
* // result is true since every file exists
|
||||||
|
* }).catch( err => {
|
||||||
|
* console.log(err);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* async.every(withMissingFileList, fileExists)
|
||||||
|
* .then( result => {
|
||||||
|
* console.log(result);
|
||||||
|
* // false
|
||||||
|
* // result is false since NOT every file exists
|
||||||
|
* }).catch( err => {
|
||||||
|
* console.log(err);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Using async/await
|
||||||
|
* async () => {
|
||||||
|
* try {
|
||||||
|
* let result = await async.every(fileList, fileExists);
|
||||||
|
* console.log(result);
|
||||||
|
* // true
|
||||||
|
* // result is true since every file exists
|
||||||
|
* }
|
||||||
|
* catch (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* async () => {
|
||||||
|
* try {
|
||||||
|
* let result = await async.every(withMissingFileList, fileExists);
|
||||||
|
* console.log(result);
|
||||||
|
* // false
|
||||||
|
* // result is false since NOT every file exists
|
||||||
|
* }
|
||||||
|
* catch (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function every(coll, iteratee, callback) {
|
||||||
|
return (0, _createTester2.default)(bool => !bool, res => !res)(_eachOf2.default, coll, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(every, 3);
|
||||||
|
module.exports = exports.default;
|
||||||
46
node_modules/async/allLimit.js
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _createTester = require('./internal/createTester.js');
|
||||||
|
|
||||||
|
var _createTester2 = _interopRequireDefault(_createTester);
|
||||||
|
|
||||||
|
var _eachOfLimit = require('./internal/eachOfLimit.js');
|
||||||
|
|
||||||
|
var _eachOfLimit2 = _interopRequireDefault(_eachOfLimit);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as [`every`]{@link module:Collections.every} but runs a maximum of `limit` async operations at a time.
|
||||||
|
*
|
||||||
|
* @name everyLimit
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @see [async.every]{@link module:Collections.every}
|
||||||
|
* @alias allLimit
|
||||||
|
* @category Collection
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {number} limit - The maximum number of async operations at a time.
|
||||||
|
* @param {AsyncFunction} iteratee - An async truth test to apply to each item
|
||||||
|
* in the collection in parallel.
|
||||||
|
* The iteratee must complete with a boolean result value.
|
||||||
|
* Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called after all the
|
||||||
|
* `iteratee` functions have finished. Result will be either `true` or `false`
|
||||||
|
* depending on the values of the async tests. Invoked with (err, result).
|
||||||
|
* @returns {Promise} a promise, if no callback provided
|
||||||
|
*/
|
||||||
|
function everyLimit(coll, limit, iteratee, callback) {
|
||||||
|
return (0, _createTester2.default)(bool => !bool, res => !res)((0, _eachOfLimit2.default)(limit), coll, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(everyLimit, 4);
|
||||||
|
module.exports = exports.default;
|
||||||
45
node_modules/async/allSeries.js
generated
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _createTester = require('./internal/createTester.js');
|
||||||
|
|
||||||
|
var _createTester2 = _interopRequireDefault(_createTester);
|
||||||
|
|
||||||
|
var _eachOfSeries = require('./eachOfSeries.js');
|
||||||
|
|
||||||
|
var _eachOfSeries2 = _interopRequireDefault(_eachOfSeries);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as [`every`]{@link module:Collections.every} but runs only a single async operation at a time.
|
||||||
|
*
|
||||||
|
* @name everySeries
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @see [async.every]{@link module:Collections.every}
|
||||||
|
* @alias allSeries
|
||||||
|
* @category Collection
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {AsyncFunction} iteratee - An async truth test to apply to each item
|
||||||
|
* in the collection in series.
|
||||||
|
* The iteratee must complete with a boolean result value.
|
||||||
|
* Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called after all the
|
||||||
|
* `iteratee` functions have finished. Result will be either `true` or `false`
|
||||||
|
* depending on the values of the async tests. Invoked with (err, result).
|
||||||
|
* @returns {Promise} a promise, if no callback provided
|
||||||
|
*/
|
||||||
|
function everySeries(coll, iteratee, callback) {
|
||||||
|
return (0, _createTester2.default)(bool => !bool, res => !res)(_eachOfSeries2.default, coll, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(everySeries, 3);
|
||||||
|
module.exports = exports.default;
|
||||||
122
node_modules/async/any.js
generated
vendored
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _createTester = require('./internal/createTester.js');
|
||||||
|
|
||||||
|
var _createTester2 = _interopRequireDefault(_createTester);
|
||||||
|
|
||||||
|
var _eachOf = require('./eachOf.js');
|
||||||
|
|
||||||
|
var _eachOf2 = _interopRequireDefault(_eachOf);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if at least one element in the `coll` satisfies an async test.
|
||||||
|
* If any iteratee call returns `true`, the main `callback` is immediately
|
||||||
|
* called.
|
||||||
|
*
|
||||||
|
* @name some
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @alias any
|
||||||
|
* @category Collection
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {AsyncFunction} iteratee - An async truth test to apply to each item
|
||||||
|
* in the collections in parallel.
|
||||||
|
* The iteratee should complete with a boolean `result` value.
|
||||||
|
* Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called as soon as any
|
||||||
|
* iteratee returns `true`, or after all the iteratee functions have finished.
|
||||||
|
* Result will be either `true` or `false` depending on the values of the async
|
||||||
|
* tests. Invoked with (err, result).
|
||||||
|
* @returns {Promise} a promise, if no callback provided
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* // dir1 is a directory that contains file1.txt, file2.txt
|
||||||
|
* // dir2 is a directory that contains file3.txt, file4.txt
|
||||||
|
* // dir3 is a directory that contains file5.txt
|
||||||
|
* // dir4 does not exist
|
||||||
|
*
|
||||||
|
* // asynchronous function that checks if a file exists
|
||||||
|
* function fileExists(file, callback) {
|
||||||
|
* fs.access(file, fs.constants.F_OK, (err) => {
|
||||||
|
* callback(null, !err);
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Using callbacks
|
||||||
|
* async.some(['dir1/missing.txt','dir2/missing.txt','dir3/file5.txt'], fileExists,
|
||||||
|
* function(err, result) {
|
||||||
|
* console.log(result);
|
||||||
|
* // true
|
||||||
|
* // result is true since some file in the list exists
|
||||||
|
* }
|
||||||
|
*);
|
||||||
|
*
|
||||||
|
* async.some(['dir1/missing.txt','dir2/missing.txt','dir4/missing.txt'], fileExists,
|
||||||
|
* function(err, result) {
|
||||||
|
* console.log(result);
|
||||||
|
* // false
|
||||||
|
* // result is false since none of the files exists
|
||||||
|
* }
|
||||||
|
*);
|
||||||
|
*
|
||||||
|
* // Using Promises
|
||||||
|
* async.some(['dir1/missing.txt','dir2/missing.txt','dir3/file5.txt'], fileExists)
|
||||||
|
* .then( result => {
|
||||||
|
* console.log(result);
|
||||||
|
* // true
|
||||||
|
* // result is true since some file in the list exists
|
||||||
|
* }).catch( err => {
|
||||||
|
* console.log(err);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* async.some(['dir1/missing.txt','dir2/missing.txt','dir4/missing.txt'], fileExists)
|
||||||
|
* .then( result => {
|
||||||
|
* console.log(result);
|
||||||
|
* // false
|
||||||
|
* // result is false since none of the files exists
|
||||||
|
* }).catch( err => {
|
||||||
|
* console.log(err);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Using async/await
|
||||||
|
* async () => {
|
||||||
|
* try {
|
||||||
|
* let result = await async.some(['dir1/missing.txt','dir2/missing.txt','dir3/file5.txt'], fileExists);
|
||||||
|
* console.log(result);
|
||||||
|
* // true
|
||||||
|
* // result is true since some file in the list exists
|
||||||
|
* }
|
||||||
|
* catch (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* async () => {
|
||||||
|
* try {
|
||||||
|
* let result = await async.some(['dir1/missing.txt','dir2/missing.txt','dir4/missing.txt'], fileExists);
|
||||||
|
* console.log(result);
|
||||||
|
* // false
|
||||||
|
* // result is false since none of the files exists
|
||||||
|
* }
|
||||||
|
* catch (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function some(coll, iteratee, callback) {
|
||||||
|
return (0, _createTester2.default)(Boolean, res => res)(_eachOf2.default, coll, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(some, 3);
|
||||||
|
module.exports = exports.default;
|
||||||
47
node_modules/async/anyLimit.js
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _createTester = require('./internal/createTester.js');
|
||||||
|
|
||||||
|
var _createTester2 = _interopRequireDefault(_createTester);
|
||||||
|
|
||||||
|
var _eachOfLimit = require('./internal/eachOfLimit.js');
|
||||||
|
|
||||||
|
var _eachOfLimit2 = _interopRequireDefault(_eachOfLimit);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as [`some`]{@link module:Collections.some} but runs a maximum of `limit` async operations at a time.
|
||||||
|
*
|
||||||
|
* @name someLimit
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @see [async.some]{@link module:Collections.some}
|
||||||
|
* @alias anyLimit
|
||||||
|
* @category Collection
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {number} limit - The maximum number of async operations at a time.
|
||||||
|
* @param {AsyncFunction} iteratee - An async truth test to apply to each item
|
||||||
|
* in the collections in parallel.
|
||||||
|
* The iteratee should complete with a boolean `result` value.
|
||||||
|
* Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called as soon as any
|
||||||
|
* iteratee returns `true`, or after all the iteratee functions have finished.
|
||||||
|
* Result will be either `true` or `false` depending on the values of the async
|
||||||
|
* tests. Invoked with (err, result).
|
||||||
|
* @returns {Promise} a promise, if no callback provided
|
||||||
|
*/
|
||||||
|
function someLimit(coll, limit, iteratee, callback) {
|
||||||
|
return (0, _createTester2.default)(Boolean, res => res)((0, _eachOfLimit2.default)(limit), coll, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(someLimit, 4);
|
||||||
|
module.exports = exports.default;
|
||||||
46
node_modules/async/anySeries.js
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _createTester = require('./internal/createTester.js');
|
||||||
|
|
||||||
|
var _createTester2 = _interopRequireDefault(_createTester);
|
||||||
|
|
||||||
|
var _eachOfSeries = require('./eachOfSeries.js');
|
||||||
|
|
||||||
|
var _eachOfSeries2 = _interopRequireDefault(_eachOfSeries);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as [`some`]{@link module:Collections.some} but runs only a single async operation at a time.
|
||||||
|
*
|
||||||
|
* @name someSeries
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @see [async.some]{@link module:Collections.some}
|
||||||
|
* @alias anySeries
|
||||||
|
* @category Collection
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {AsyncFunction} iteratee - An async truth test to apply to each item
|
||||||
|
* in the collections in series.
|
||||||
|
* The iteratee should complete with a boolean `result` value.
|
||||||
|
* Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called as soon as any
|
||||||
|
* iteratee returns `true`, or after all the iteratee functions have finished.
|
||||||
|
* Result will be either `true` or `false` depending on the values of the async
|
||||||
|
* tests. Invoked with (err, result).
|
||||||
|
* @returns {Promise} a promise, if no callback provided
|
||||||
|
*/
|
||||||
|
function someSeries(coll, iteratee, callback) {
|
||||||
|
return (0, _createTester2.default)(Boolean, res => res)(_eachOfSeries2.default, coll, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(someSeries, 3);
|
||||||
|
module.exports = exports.default;
|
||||||
11
node_modules/async/apply.js
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.default = function (fn, ...args) {
|
||||||
|
return (...callArgs) => fn(...args, ...callArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = exports.default;
|
||||||
57
node_modules/async/applyEach.js
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _applyEach = require('./internal/applyEach.js');
|
||||||
|
|
||||||
|
var _applyEach2 = _interopRequireDefault(_applyEach);
|
||||||
|
|
||||||
|
var _map = require('./map.js');
|
||||||
|
|
||||||
|
var _map2 = _interopRequireDefault(_map);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the provided arguments to each function in the array, calling
|
||||||
|
* `callback` after all functions have completed. If you only provide the first
|
||||||
|
* argument, `fns`, then it will return a function which lets you pass in the
|
||||||
|
* arguments as if it were a single function call. If more arguments are
|
||||||
|
* provided, `callback` is required while `args` is still optional. The results
|
||||||
|
* for each of the applied async functions are passed to the final callback
|
||||||
|
* as an array.
|
||||||
|
*
|
||||||
|
* @name applyEach
|
||||||
|
* @static
|
||||||
|
* @memberOf module:ControlFlow
|
||||||
|
* @method
|
||||||
|
* @category Control Flow
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} fns - A collection of {@link AsyncFunction}s
|
||||||
|
* to all call with the same arguments
|
||||||
|
* @param {...*} [args] - any number of separate arguments to pass to the
|
||||||
|
* function.
|
||||||
|
* @param {Function} [callback] - the final argument should be the callback,
|
||||||
|
* called when all functions have completed processing.
|
||||||
|
* @returns {AsyncFunction} - Returns a function that takes no args other than
|
||||||
|
* an optional callback, that is the result of applying the `args` to each
|
||||||
|
* of the functions.
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* const appliedFn = async.applyEach([enableSearch, updateSchema], 'bucket')
|
||||||
|
*
|
||||||
|
* appliedFn((err, results) => {
|
||||||
|
* // results[0] is the results for `enableSearch`
|
||||||
|
* // results[1] is the results for `updateSchema`
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // partial application example:
|
||||||
|
* async.each(
|
||||||
|
* buckets,
|
||||||
|
* async (bucket) => async.applyEach([enableSearch, updateSchema], bucket)(),
|
||||||
|
* callback
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
exports.default = (0, _applyEach2.default)(_map2.default);
|
||||||
|
module.exports = exports.default;
|
||||||
37
node_modules/async/applyEachSeries.js
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _applyEach = require('./internal/applyEach.js');
|
||||||
|
|
||||||
|
var _applyEach2 = _interopRequireDefault(_applyEach);
|
||||||
|
|
||||||
|
var _mapSeries = require('./mapSeries.js');
|
||||||
|
|
||||||
|
var _mapSeries2 = _interopRequireDefault(_mapSeries);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as [`applyEach`]{@link module:ControlFlow.applyEach} but runs only a single async operation at a time.
|
||||||
|
*
|
||||||
|
* @name applyEachSeries
|
||||||
|
* @static
|
||||||
|
* @memberOf module:ControlFlow
|
||||||
|
* @method
|
||||||
|
* @see [async.applyEach]{@link module:ControlFlow.applyEach}
|
||||||
|
* @category Control Flow
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} fns - A collection of {@link AsyncFunction}s to all
|
||||||
|
* call with the same arguments
|
||||||
|
* @param {...*} [args] - any number of separate arguments to pass to the
|
||||||
|
* function.
|
||||||
|
* @param {Function} [callback] - the final argument should be the callback,
|
||||||
|
* called when all functions have completed processing.
|
||||||
|
* @returns {AsyncFunction} - A function, that when called, is the result of
|
||||||
|
* appling the `args` to the list of functions. It takes no args, other than
|
||||||
|
* a callback.
|
||||||
|
*/
|
||||||
|
exports.default = (0, _applyEach2.default)(_mapSeries2.default);
|
||||||
|
module.exports = exports.default;
|
||||||
118
node_modules/async/asyncify.js
generated
vendored
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = asyncify;
|
||||||
|
|
||||||
|
var _initialParams = require('./internal/initialParams.js');
|
||||||
|
|
||||||
|
var _initialParams2 = _interopRequireDefault(_initialParams);
|
||||||
|
|
||||||
|
var _setImmediate = require('./internal/setImmediate.js');
|
||||||
|
|
||||||
|
var _setImmediate2 = _interopRequireDefault(_setImmediate);
|
||||||
|
|
||||||
|
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a sync function and make it async, passing its return value to a
|
||||||
|
* callback. This is useful for plugging sync functions into a waterfall,
|
||||||
|
* series, or other async functions. Any arguments passed to the generated
|
||||||
|
* function will be passed to the wrapped function (except for the final
|
||||||
|
* callback argument). Errors thrown will be passed to the callback.
|
||||||
|
*
|
||||||
|
* If the function passed to `asyncify` returns a Promise, that promises's
|
||||||
|
* resolved/rejected state will be used to call the callback, rather than simply
|
||||||
|
* the synchronous return value.
|
||||||
|
*
|
||||||
|
* This also means you can asyncify ES2017 `async` functions.
|
||||||
|
*
|
||||||
|
* @name asyncify
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Utils
|
||||||
|
* @method
|
||||||
|
* @alias wrapSync
|
||||||
|
* @category Util
|
||||||
|
* @param {Function} func - The synchronous function, or Promise-returning
|
||||||
|
* function to convert to an {@link AsyncFunction}.
|
||||||
|
* @returns {AsyncFunction} An asynchronous wrapper of the `func`. To be
|
||||||
|
* invoked with `(args..., callback)`.
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* // passing a regular synchronous function
|
||||||
|
* async.waterfall([
|
||||||
|
* async.apply(fs.readFile, filename, "utf8"),
|
||||||
|
* async.asyncify(JSON.parse),
|
||||||
|
* function (data, next) {
|
||||||
|
* // data is the result of parsing the text.
|
||||||
|
* // If there was a parsing error, it would have been caught.
|
||||||
|
* }
|
||||||
|
* ], callback);
|
||||||
|
*
|
||||||
|
* // passing a function returning a promise
|
||||||
|
* async.waterfall([
|
||||||
|
* async.apply(fs.readFile, filename, "utf8"),
|
||||||
|
* async.asyncify(function (contents) {
|
||||||
|
* return db.model.create(contents);
|
||||||
|
* }),
|
||||||
|
* function (model, next) {
|
||||||
|
* // `model` is the instantiated model object.
|
||||||
|
* // If there was an error, this function would be skipped.
|
||||||
|
* }
|
||||||
|
* ], callback);
|
||||||
|
*
|
||||||
|
* // es2017 example, though `asyncify` is not needed if your JS environment
|
||||||
|
* // supports async functions out of the box
|
||||||
|
* var q = async.queue(async.asyncify(async function(file) {
|
||||||
|
* var intermediateStep = await processFile(file);
|
||||||
|
* return await somePromise(intermediateStep)
|
||||||
|
* }));
|
||||||
|
*
|
||||||
|
* q.push(files);
|
||||||
|
*/
|
||||||
|
function asyncify(func) {
|
||||||
|
if ((0, _wrapAsync.isAsync)(func)) {
|
||||||
|
return function (...args /*, callback*/) {
|
||||||
|
const callback = args.pop();
|
||||||
|
const promise = func.apply(this, args);
|
||||||
|
return handlePromise(promise, callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, _initialParams2.default)(function (args, callback) {
|
||||||
|
var result;
|
||||||
|
try {
|
||||||
|
result = func.apply(this, args);
|
||||||
|
} catch (e) {
|
||||||
|
return callback(e);
|
||||||
|
}
|
||||||
|
// if result is Promise object
|
||||||
|
if (result && typeof result.then === 'function') {
|
||||||
|
return handlePromise(result, callback);
|
||||||
|
} else {
|
||||||
|
callback(null, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePromise(promise, callback) {
|
||||||
|
return promise.then(value => {
|
||||||
|
invokeCallback(callback, null, value);
|
||||||
|
}, err => {
|
||||||
|
invokeCallback(callback, err && (err instanceof Error || err.message) ? err : new Error(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeCallback(callback, error, value) {
|
||||||
|
try {
|
||||||
|
callback(error, value);
|
||||||
|
} catch (err) {
|
||||||
|
(0, _setImmediate2.default)(e => {
|
||||||
|
throw e;
|
||||||
|
}, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = exports.default;
|
||||||
333
node_modules/async/auto.js
generated
vendored
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = auto;
|
||||||
|
|
||||||
|
var _once = require('./internal/once.js');
|
||||||
|
|
||||||
|
var _once2 = _interopRequireDefault(_once);
|
||||||
|
|
||||||
|
var _onlyOnce = require('./internal/onlyOnce.js');
|
||||||
|
|
||||||
|
var _onlyOnce2 = _interopRequireDefault(_onlyOnce);
|
||||||
|
|
||||||
|
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||||
|
|
||||||
|
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||||
|
|
||||||
|
var _promiseCallback = require('./internal/promiseCallback.js');
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the best order for running the {@link AsyncFunction}s in `tasks`, based on
|
||||||
|
* their requirements. Each function can optionally depend on other functions
|
||||||
|
* being completed first, and each function is run as soon as its requirements
|
||||||
|
* are satisfied.
|
||||||
|
*
|
||||||
|
* If any of the {@link AsyncFunction}s pass an error to their callback, the `auto` sequence
|
||||||
|
* will stop. Further tasks will not execute (so any other functions depending
|
||||||
|
* on it will not run), and the main `callback` is immediately called with the
|
||||||
|
* error.
|
||||||
|
*
|
||||||
|
* {@link AsyncFunction}s also receive an object containing the results of functions which
|
||||||
|
* have completed so far as the first argument, if they have dependencies. If a
|
||||||
|
* task function has no dependencies, it will only be passed a callback.
|
||||||
|
*
|
||||||
|
* @name auto
|
||||||
|
* @static
|
||||||
|
* @memberOf module:ControlFlow
|
||||||
|
* @method
|
||||||
|
* @category Control Flow
|
||||||
|
* @param {Object} tasks - An object. Each of its properties is either a
|
||||||
|
* function or an array of requirements, with the {@link AsyncFunction} itself the last item
|
||||||
|
* in the array. The object's key of a property serves as the name of the task
|
||||||
|
* defined by that property, i.e. can be used when specifying requirements for
|
||||||
|
* other tasks. The function receives one or two arguments:
|
||||||
|
* * a `results` object, containing the results of the previously executed
|
||||||
|
* functions, only passed if the task has any dependencies,
|
||||||
|
* * a `callback(err, result)` function, which must be called when finished,
|
||||||
|
* passing an `error` (which can be `null`) and the result of the function's
|
||||||
|
* execution.
|
||||||
|
* @param {number} [concurrency=Infinity] - An optional `integer` for
|
||||||
|
* determining the maximum number of tasks that can be run in parallel. By
|
||||||
|
* default, as many as possible.
|
||||||
|
* @param {Function} [callback] - An optional callback which is called when all
|
||||||
|
* the tasks have been completed. It receives the `err` argument if any `tasks`
|
||||||
|
* pass an error to their callback. Results are always returned; however, if an
|
||||||
|
* error occurs, no further `tasks` will be performed, and the results object
|
||||||
|
* will only contain partial results. Invoked with (err, results).
|
||||||
|
* @returns {Promise} a promise, if a callback is not passed
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* //Using Callbacks
|
||||||
|
* async.auto({
|
||||||
|
* get_data: function(callback) {
|
||||||
|
* // async code to get some data
|
||||||
|
* callback(null, 'data', 'converted to array');
|
||||||
|
* },
|
||||||
|
* make_folder: function(callback) {
|
||||||
|
* // async code to create a directory to store a file in
|
||||||
|
* // this is run at the same time as getting the data
|
||||||
|
* callback(null, 'folder');
|
||||||
|
* },
|
||||||
|
* write_file: ['get_data', 'make_folder', function(results, callback) {
|
||||||
|
* // once there is some data and the directory exists,
|
||||||
|
* // write the data to a file in the directory
|
||||||
|
* callback(null, 'filename');
|
||||||
|
* }],
|
||||||
|
* email_link: ['write_file', function(results, callback) {
|
||||||
|
* // once the file is written let's email a link to it...
|
||||||
|
* callback(null, {'file':results.write_file, 'email':'user@example.com'});
|
||||||
|
* }]
|
||||||
|
* }, function(err, results) {
|
||||||
|
* if (err) {
|
||||||
|
* console.log('err = ', err);
|
||||||
|
* }
|
||||||
|
* console.log('results = ', results);
|
||||||
|
* // results = {
|
||||||
|
* // get_data: ['data', 'converted to array']
|
||||||
|
* // make_folder; 'folder',
|
||||||
|
* // write_file: 'filename'
|
||||||
|
* // email_link: { file: 'filename', email: 'user@example.com' }
|
||||||
|
* // }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* //Using Promises
|
||||||
|
* async.auto({
|
||||||
|
* get_data: function(callback) {
|
||||||
|
* console.log('in get_data');
|
||||||
|
* // async code to get some data
|
||||||
|
* callback(null, 'data', 'converted to array');
|
||||||
|
* },
|
||||||
|
* make_folder: function(callback) {
|
||||||
|
* console.log('in make_folder');
|
||||||
|
* // async code to create a directory to store a file in
|
||||||
|
* // this is run at the same time as getting the data
|
||||||
|
* callback(null, 'folder');
|
||||||
|
* },
|
||||||
|
* write_file: ['get_data', 'make_folder', function(results, callback) {
|
||||||
|
* // once there is some data and the directory exists,
|
||||||
|
* // write the data to a file in the directory
|
||||||
|
* callback(null, 'filename');
|
||||||
|
* }],
|
||||||
|
* email_link: ['write_file', function(results, callback) {
|
||||||
|
* // once the file is written let's email a link to it...
|
||||||
|
* callback(null, {'file':results.write_file, 'email':'user@example.com'});
|
||||||
|
* }]
|
||||||
|
* }).then(results => {
|
||||||
|
* console.log('results = ', results);
|
||||||
|
* // results = {
|
||||||
|
* // get_data: ['data', 'converted to array']
|
||||||
|
* // make_folder; 'folder',
|
||||||
|
* // write_file: 'filename'
|
||||||
|
* // email_link: { file: 'filename', email: 'user@example.com' }
|
||||||
|
* // }
|
||||||
|
* }).catch(err => {
|
||||||
|
* console.log('err = ', err);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* //Using async/await
|
||||||
|
* async () => {
|
||||||
|
* try {
|
||||||
|
* let results = await async.auto({
|
||||||
|
* get_data: function(callback) {
|
||||||
|
* // async code to get some data
|
||||||
|
* callback(null, 'data', 'converted to array');
|
||||||
|
* },
|
||||||
|
* make_folder: function(callback) {
|
||||||
|
* // async code to create a directory to store a file in
|
||||||
|
* // this is run at the same time as getting the data
|
||||||
|
* callback(null, 'folder');
|
||||||
|
* },
|
||||||
|
* write_file: ['get_data', 'make_folder', function(results, callback) {
|
||||||
|
* // once there is some data and the directory exists,
|
||||||
|
* // write the data to a file in the directory
|
||||||
|
* callback(null, 'filename');
|
||||||
|
* }],
|
||||||
|
* email_link: ['write_file', function(results, callback) {
|
||||||
|
* // once the file is written let's email a link to it...
|
||||||
|
* callback(null, {'file':results.write_file, 'email':'user@example.com'});
|
||||||
|
* }]
|
||||||
|
* });
|
||||||
|
* console.log('results = ', results);
|
||||||
|
* // results = {
|
||||||
|
* // get_data: ['data', 'converted to array']
|
||||||
|
* // make_folder; 'folder',
|
||||||
|
* // write_file: 'filename'
|
||||||
|
* // email_link: { file: 'filename', email: 'user@example.com' }
|
||||||
|
* // }
|
||||||
|
* }
|
||||||
|
* catch (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function auto(tasks, concurrency, callback) {
|
||||||
|
if (typeof concurrency !== 'number') {
|
||||||
|
// concurrency is optional, shift the args.
|
||||||
|
callback = concurrency;
|
||||||
|
concurrency = null;
|
||||||
|
}
|
||||||
|
callback = (0, _once2.default)(callback || (0, _promiseCallback.promiseCallback)());
|
||||||
|
var numTasks = Object.keys(tasks).length;
|
||||||
|
if (!numTasks) {
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
if (!concurrency) {
|
||||||
|
concurrency = numTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = {};
|
||||||
|
var runningTasks = 0;
|
||||||
|
var canceled = false;
|
||||||
|
var hasError = false;
|
||||||
|
|
||||||
|
var listeners = Object.create(null);
|
||||||
|
|
||||||
|
var readyTasks = [];
|
||||||
|
|
||||||
|
// for cycle detection:
|
||||||
|
var readyToCheck = []; // tasks that have been identified as reachable
|
||||||
|
// without the possibility of returning to an ancestor task
|
||||||
|
var uncheckedDependencies = {};
|
||||||
|
|
||||||
|
Object.keys(tasks).forEach(key => {
|
||||||
|
var task = tasks[key];
|
||||||
|
if (!Array.isArray(task)) {
|
||||||
|
// no dependencies
|
||||||
|
enqueueTask(key, [task]);
|
||||||
|
readyToCheck.push(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dependencies = task.slice(0, task.length - 1);
|
||||||
|
var remainingDependencies = dependencies.length;
|
||||||
|
if (remainingDependencies === 0) {
|
||||||
|
enqueueTask(key, task);
|
||||||
|
readyToCheck.push(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uncheckedDependencies[key] = remainingDependencies;
|
||||||
|
|
||||||
|
dependencies.forEach(dependencyName => {
|
||||||
|
if (!tasks[dependencyName]) {
|
||||||
|
throw new Error('async.auto task `' + key + '` has a non-existent dependency `' + dependencyName + '` in ' + dependencies.join(', '));
|
||||||
|
}
|
||||||
|
addListener(dependencyName, () => {
|
||||||
|
remainingDependencies--;
|
||||||
|
if (remainingDependencies === 0) {
|
||||||
|
enqueueTask(key, task);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
checkForDeadlocks();
|
||||||
|
processQueue();
|
||||||
|
|
||||||
|
function enqueueTask(key, task) {
|
||||||
|
readyTasks.push(() => runTask(key, task));
|
||||||
|
}
|
||||||
|
|
||||||
|
function processQueue() {
|
||||||
|
if (canceled) return;
|
||||||
|
if (readyTasks.length === 0 && runningTasks === 0) {
|
||||||
|
return callback(null, results);
|
||||||
|
}
|
||||||
|
while (readyTasks.length && runningTasks < concurrency) {
|
||||||
|
var run = readyTasks.shift();
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addListener(taskName, fn) {
|
||||||
|
var taskListeners = listeners[taskName];
|
||||||
|
if (!taskListeners) {
|
||||||
|
taskListeners = listeners[taskName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
taskListeners.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskComplete(taskName) {
|
||||||
|
var taskListeners = listeners[taskName] || [];
|
||||||
|
taskListeners.forEach(fn => fn());
|
||||||
|
processQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTask(key, task) {
|
||||||
|
if (hasError) return;
|
||||||
|
|
||||||
|
var taskCallback = (0, _onlyOnce2.default)((err, ...result) => {
|
||||||
|
runningTasks--;
|
||||||
|
if (err === false) {
|
||||||
|
canceled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.length < 2) {
|
||||||
|
[result] = result;
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
var safeResults = {};
|
||||||
|
Object.keys(results).forEach(rkey => {
|
||||||
|
safeResults[rkey] = results[rkey];
|
||||||
|
});
|
||||||
|
safeResults[key] = result;
|
||||||
|
hasError = true;
|
||||||
|
listeners = Object.create(null);
|
||||||
|
if (canceled) return;
|
||||||
|
callback(err, safeResults);
|
||||||
|
} else {
|
||||||
|
results[key] = result;
|
||||||
|
taskComplete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runningTasks++;
|
||||||
|
var taskFn = (0, _wrapAsync2.default)(task[task.length - 1]);
|
||||||
|
if (task.length > 1) {
|
||||||
|
taskFn(results, taskCallback);
|
||||||
|
} else {
|
||||||
|
taskFn(taskCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForDeadlocks() {
|
||||||
|
// Kahn's algorithm
|
||||||
|
// https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
|
||||||
|
// http://connalle.blogspot.com/2013/10/topological-sortingkahn-algorithm.html
|
||||||
|
var currentTask;
|
||||||
|
var counter = 0;
|
||||||
|
while (readyToCheck.length) {
|
||||||
|
currentTask = readyToCheck.pop();
|
||||||
|
counter++;
|
||||||
|
getDependents(currentTask).forEach(dependent => {
|
||||||
|
if (--uncheckedDependencies[dependent] === 0) {
|
||||||
|
readyToCheck.push(dependent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counter !== numTasks) {
|
||||||
|
throw new Error('async.auto cannot execute tasks due to a recursive dependency');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDependents(taskName) {
|
||||||
|
var result = [];
|
||||||
|
Object.keys(tasks).forEach(key => {
|
||||||
|
const task = tasks[key];
|
||||||
|
if (Array.isArray(task) && task.indexOf(taskName) >= 0) {
|
||||||
|
result.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback[_promiseCallback.PROMISE_SYMBOL];
|
||||||
|
}
|
||||||
|
module.exports = exports.default;
|
||||||
182
node_modules/async/autoInject.js
generated
vendored
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = autoInject;
|
||||||
|
|
||||||
|
var _auto = require('./auto.js');
|
||||||
|
|
||||||
|
var _auto2 = _interopRequireDefault(_auto);
|
||||||
|
|
||||||
|
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||||
|
|
||||||
|
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
var FN_ARGS = /^(?:async\s)?(?:function)?\s*(?:\w+\s*)?\(([^)]+)\)(?:\s*{)/;
|
||||||
|
var ARROW_FN_ARGS = /^(?:async\s)?\s*(?:\(\s*)?((?:[^)=\s]\s*)*)(?:\)\s*)?=>/;
|
||||||
|
var FN_ARG_SPLIT = /,/;
|
||||||
|
var FN_ARG = /(=.+)?(\s*)$/;
|
||||||
|
|
||||||
|
function stripComments(string) {
|
||||||
|
let stripped = '';
|
||||||
|
let index = 0;
|
||||||
|
let endBlockComment = string.indexOf('*/');
|
||||||
|
while (index < string.length) {
|
||||||
|
if (string[index] === '/' && string[index + 1] === '/') {
|
||||||
|
// inline comment
|
||||||
|
let endIndex = string.indexOf('\n', index);
|
||||||
|
index = endIndex === -1 ? string.length : endIndex;
|
||||||
|
} else if (endBlockComment !== -1 && string[index] === '/' && string[index + 1] === '*') {
|
||||||
|
// block comment
|
||||||
|
let endIndex = string.indexOf('*/', index);
|
||||||
|
if (endIndex !== -1) {
|
||||||
|
index = endIndex + 2;
|
||||||
|
endBlockComment = string.indexOf('*/', index);
|
||||||
|
} else {
|
||||||
|
stripped += string[index];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stripped += string[index];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseParams(func) {
|
||||||
|
const src = stripComments(func.toString());
|
||||||
|
let match = src.match(FN_ARGS);
|
||||||
|
if (!match) {
|
||||||
|
match = src.match(ARROW_FN_ARGS);
|
||||||
|
}
|
||||||
|
if (!match) throw new Error('could not parse args in autoInject\nSource:\n' + src);
|
||||||
|
let [, args] = match;
|
||||||
|
return args.replace(/\s/g, '').split(FN_ARG_SPLIT).map(arg => arg.replace(FN_ARG, '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dependency-injected version of the [async.auto]{@link module:ControlFlow.auto} function. Dependent
|
||||||
|
* tasks are specified as parameters to the function, after the usual callback
|
||||||
|
* parameter, with the parameter names matching the names of the tasks it
|
||||||
|
* depends on. This can provide even more readable task graphs which can be
|
||||||
|
* easier to maintain.
|
||||||
|
*
|
||||||
|
* If a final callback is specified, the task results are similarly injected,
|
||||||
|
* specified as named parameters after the initial error parameter.
|
||||||
|
*
|
||||||
|
* The autoInject function is purely syntactic sugar and its semantics are
|
||||||
|
* otherwise equivalent to [async.auto]{@link module:ControlFlow.auto}.
|
||||||
|
*
|
||||||
|
* @name autoInject
|
||||||
|
* @static
|
||||||
|
* @memberOf module:ControlFlow
|
||||||
|
* @method
|
||||||
|
* @see [async.auto]{@link module:ControlFlow.auto}
|
||||||
|
* @category Control Flow
|
||||||
|
* @param {Object} tasks - An object, each of whose properties is an {@link AsyncFunction} of
|
||||||
|
* the form 'func([dependencies...], callback). The object's key of a property
|
||||||
|
* serves as the name of the task defined by that property, i.e. can be used
|
||||||
|
* when specifying requirements for other tasks.
|
||||||
|
* * The `callback` parameter is a `callback(err, result)` which must be called
|
||||||
|
* when finished, passing an `error` (which can be `null`) and the result of
|
||||||
|
* the function's execution. The remaining parameters name other tasks on
|
||||||
|
* which the task is dependent, and the results from those tasks are the
|
||||||
|
* arguments of those parameters.
|
||||||
|
* @param {Function} [callback] - An optional callback which is called when all
|
||||||
|
* the tasks have been completed. It receives the `err` argument if any `tasks`
|
||||||
|
* pass an error to their callback, and a `results` object with any completed
|
||||||
|
* task results, similar to `auto`.
|
||||||
|
* @returns {Promise} a promise, if no callback is passed
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* // The example from `auto` can be rewritten as follows:
|
||||||
|
* async.autoInject({
|
||||||
|
* get_data: function(callback) {
|
||||||
|
* // async code to get some data
|
||||||
|
* callback(null, 'data', 'converted to array');
|
||||||
|
* },
|
||||||
|
* make_folder: function(callback) {
|
||||||
|
* // async code to create a directory to store a file in
|
||||||
|
* // this is run at the same time as getting the data
|
||||||
|
* callback(null, 'folder');
|
||||||
|
* },
|
||||||
|
* write_file: function(get_data, make_folder, callback) {
|
||||||
|
* // once there is some data and the directory exists,
|
||||||
|
* // write the data to a file in the directory
|
||||||
|
* callback(null, 'filename');
|
||||||
|
* },
|
||||||
|
* email_link: function(write_file, callback) {
|
||||||
|
* // once the file is written let's email a link to it...
|
||||||
|
* // write_file contains the filename returned by write_file.
|
||||||
|
* callback(null, {'file':write_file, 'email':'user@example.com'});
|
||||||
|
* }
|
||||||
|
* }, function(err, results) {
|
||||||
|
* console.log('err = ', err);
|
||||||
|
* console.log('email_link = ', results.email_link);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // If you are using a JS minifier that mangles parameter names, `autoInject`
|
||||||
|
* // will not work with plain functions, since the parameter names will be
|
||||||
|
* // collapsed to a single letter identifier. To work around this, you can
|
||||||
|
* // explicitly specify the names of the parameters your task function needs
|
||||||
|
* // in an array, similar to Angular.js dependency injection.
|
||||||
|
*
|
||||||
|
* // This still has an advantage over plain `auto`, since the results a task
|
||||||
|
* // depends on are still spread into arguments.
|
||||||
|
* async.autoInject({
|
||||||
|
* //...
|
||||||
|
* write_file: ['get_data', 'make_folder', function(get_data, make_folder, callback) {
|
||||||
|
* callback(null, 'filename');
|
||||||
|
* }],
|
||||||
|
* email_link: ['write_file', function(write_file, callback) {
|
||||||
|
* callback(null, {'file':write_file, 'email':'user@example.com'});
|
||||||
|
* }]
|
||||||
|
* //...
|
||||||
|
* }, function(err, results) {
|
||||||
|
* console.log('err = ', err);
|
||||||
|
* console.log('email_link = ', results.email_link);
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
function autoInject(tasks, callback) {
|
||||||
|
var newTasks = {};
|
||||||
|
|
||||||
|
Object.keys(tasks).forEach(key => {
|
||||||
|
var taskFn = tasks[key];
|
||||||
|
var params;
|
||||||
|
var fnIsAsync = (0, _wrapAsync.isAsync)(taskFn);
|
||||||
|
var hasNoDeps = !fnIsAsync && taskFn.length === 1 || fnIsAsync && taskFn.length === 0;
|
||||||
|
|
||||||
|
if (Array.isArray(taskFn)) {
|
||||||
|
params = [...taskFn];
|
||||||
|
taskFn = params.pop();
|
||||||
|
|
||||||
|
newTasks[key] = params.concat(params.length > 0 ? newTask : taskFn);
|
||||||
|
} else if (hasNoDeps) {
|
||||||
|
// no dependencies, use the function as-is
|
||||||
|
newTasks[key] = taskFn;
|
||||||
|
} else {
|
||||||
|
params = parseParams(taskFn);
|
||||||
|
if (taskFn.length === 0 && !fnIsAsync && params.length === 0) {
|
||||||
|
throw new Error("autoInject task functions require explicit parameters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove callback param
|
||||||
|
if (!fnIsAsync) params.pop();
|
||||||
|
|
||||||
|
newTasks[key] = params.concat(newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
function newTask(results, taskCb) {
|
||||||
|
var newArgs = params.map(name => results[name]);
|
||||||
|
newArgs.push(taskCb);
|
||||||
|
(0, _wrapAsync2.default)(taskFn)(...newArgs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (0, _auto2.default)(newTasks, callback);
|
||||||
|
}
|
||||||
|
module.exports = exports.default;
|
||||||
17
node_modules/async/bower.json
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "async",
|
||||||
|
"main": "dist/async.js",
|
||||||
|
"ignore": [
|
||||||
|
"bower_components",
|
||||||
|
"lib",
|
||||||
|
"test",
|
||||||
|
"node_modules",
|
||||||
|
"perf",
|
||||||
|
"support",
|
||||||
|
"**/.*",
|
||||||
|
"*.config.js",
|
||||||
|
"*.json",
|
||||||
|
"index.js",
|
||||||
|
"Makefile"
|
||||||
|
]
|
||||||
|
}
|
||||||
63
node_modules/async/cargo.js
generated
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = cargo;
|
||||||
|
|
||||||
|
var _queue = require('./internal/queue.js');
|
||||||
|
|
||||||
|
var _queue2 = _interopRequireDefault(_queue);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a `cargo` object with the specified payload. Tasks added to the
|
||||||
|
* cargo will be processed altogether (up to the `payload` limit). If the
|
||||||
|
* `worker` is in progress, the task is queued until it becomes available. Once
|
||||||
|
* the `worker` has completed some tasks, each callback of those tasks is
|
||||||
|
* called. Check out [these](https://camo.githubusercontent.com/6bbd36f4cf5b35a0f11a96dcd2e97711ffc2fb37/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130382f62626330636662302d356632392d313165322d393734662d3333393763363464633835382e676966) [animations](https://camo.githubusercontent.com/f4810e00e1c5f5f8addbe3e9f49064fd5d102699/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130312f38346339323036362d356632392d313165322d383134662d3964336430323431336266642e676966)
|
||||||
|
* for how `cargo` and `queue` work.
|
||||||
|
*
|
||||||
|
* While [`queue`]{@link module:ControlFlow.queue} passes only one task to one of a group of workers
|
||||||
|
* at a time, cargo passes an array of tasks to a single worker, repeating
|
||||||
|
* when the worker is finished.
|
||||||
|
*
|
||||||
|
* @name cargo
|
||||||
|
* @static
|
||||||
|
* @memberOf module:ControlFlow
|
||||||
|
* @method
|
||||||
|
* @see [async.queue]{@link module:ControlFlow.queue}
|
||||||
|
* @category Control Flow
|
||||||
|
* @param {AsyncFunction} worker - An asynchronous function for processing an array
|
||||||
|
* of queued tasks. Invoked with `(tasks, callback)`.
|
||||||
|
* @param {number} [payload=Infinity] - An optional `integer` for determining
|
||||||
|
* how many tasks should be processed per round; if omitted, the default is
|
||||||
|
* unlimited.
|
||||||
|
* @returns {module:ControlFlow.QueueObject} A cargo object to manage the tasks. Callbacks can
|
||||||
|
* attached as certain properties to listen for specific events during the
|
||||||
|
* lifecycle of the cargo and inner queue.
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* // create a cargo object with payload 2
|
||||||
|
* var cargo = async.cargo(function(tasks, callback) {
|
||||||
|
* for (var i=0; i<tasks.length; i++) {
|
||||||
|
* console.log('hello ' + tasks[i].name);
|
||||||
|
* }
|
||||||
|
* callback();
|
||||||
|
* }, 2);
|
||||||
|
*
|
||||||
|
* // add some items
|
||||||
|
* cargo.push({name: 'foo'}, function(err) {
|
||||||
|
* console.log('finished processing foo');
|
||||||
|
* });
|
||||||
|
* cargo.push({name: 'bar'}, function(err) {
|
||||||
|
* console.log('finished processing bar');
|
||||||
|
* });
|
||||||
|
* await cargo.push({name: 'baz'});
|
||||||
|
* console.log('finished processing baz');
|
||||||
|
*/
|
||||||
|
function cargo(worker, payload) {
|
||||||
|
return (0, _queue2.default)(worker, 1, payload);
|
||||||
|
}
|
||||||
|
module.exports = exports.default;
|
||||||
71
node_modules/async/cargoQueue.js
generated
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = cargo;
|
||||||
|
|
||||||
|
var _queue = require('./internal/queue.js');
|
||||||
|
|
||||||
|
var _queue2 = _interopRequireDefault(_queue);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a `cargoQueue` object with the specified payload. Tasks added to the
|
||||||
|
* cargoQueue will be processed together (up to the `payload` limit) in `concurrency` parallel workers.
|
||||||
|
* If the all `workers` are in progress, the task is queued until one becomes available. Once
|
||||||
|
* a `worker` has completed some tasks, each callback of those tasks is
|
||||||
|
* called. Check out [these](https://camo.githubusercontent.com/6bbd36f4cf5b35a0f11a96dcd2e97711ffc2fb37/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130382f62626330636662302d356632392d313165322d393734662d3333393763363464633835382e676966) [animations](https://camo.githubusercontent.com/f4810e00e1c5f5f8addbe3e9f49064fd5d102699/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130312f38346339323036362d356632392d313165322d383134662d3964336430323431336266642e676966)
|
||||||
|
* for how `cargo` and `queue` work.
|
||||||
|
*
|
||||||
|
* While [`queue`]{@link module:ControlFlow.queue} passes only one task to one of a group of workers
|
||||||
|
* at a time, and [`cargo`]{@link module:ControlFlow.cargo} passes an array of tasks to a single worker,
|
||||||
|
* the cargoQueue passes an array of tasks to multiple parallel workers.
|
||||||
|
*
|
||||||
|
* @name cargoQueue
|
||||||
|
* @static
|
||||||
|
* @memberOf module:ControlFlow
|
||||||
|
* @method
|
||||||
|
* @see [async.queue]{@link module:ControlFlow.queue}
|
||||||
|
* @see [async.cargo]{@link module:ControlFLow.cargo}
|
||||||
|
* @category Control Flow
|
||||||
|
* @param {AsyncFunction} worker - An asynchronous function for processing an array
|
||||||
|
* of queued tasks. Invoked with `(tasks, callback)`.
|
||||||
|
* @param {number} [concurrency=1] - An `integer` for determining how many
|
||||||
|
* `worker` functions should be run in parallel. If omitted, the concurrency
|
||||||
|
* defaults to `1`. If the concurrency is `0`, an error is thrown.
|
||||||
|
* @param {number} [payload=Infinity] - An optional `integer` for determining
|
||||||
|
* how many tasks should be processed per round; if omitted, the default is
|
||||||
|
* unlimited.
|
||||||
|
* @returns {module:ControlFlow.QueueObject} A cargoQueue object to manage the tasks. Callbacks can
|
||||||
|
* attached as certain properties to listen for specific events during the
|
||||||
|
* lifecycle of the cargoQueue and inner queue.
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* // create a cargoQueue object with payload 2 and concurrency 2
|
||||||
|
* var cargoQueue = async.cargoQueue(function(tasks, callback) {
|
||||||
|
* for (var i=0; i<tasks.length; i++) {
|
||||||
|
* console.log('hello ' + tasks[i].name);
|
||||||
|
* }
|
||||||
|
* callback();
|
||||||
|
* }, 2, 2);
|
||||||
|
*
|
||||||
|
* // add some items
|
||||||
|
* cargoQueue.push({name: 'foo'}, function(err) {
|
||||||
|
* console.log('finished processing foo');
|
||||||
|
* });
|
||||||
|
* cargoQueue.push({name: 'bar'}, function(err) {
|
||||||
|
* console.log('finished processing bar');
|
||||||
|
* });
|
||||||
|
* cargoQueue.push({name: 'baz'}, function(err) {
|
||||||
|
* console.log('finished processing baz');
|
||||||
|
* });
|
||||||
|
* cargoQueue.push({name: 'boo'}, function(err) {
|
||||||
|
* console.log('finished processing boo');
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
function cargo(worker, concurrency, payload) {
|
||||||
|
return (0, _queue2.default)(worker, concurrency, payload);
|
||||||
|
}
|
||||||
|
module.exports = exports.default;
|
||||||
55
node_modules/async/compose.js
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
exports.default = compose;
|
||||||
|
|
||||||
|
var _seq = require('./seq.js');
|
||||||
|
|
||||||
|
var _seq2 = _interopRequireDefault(_seq);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function which is a composition of the passed asynchronous
|
||||||
|
* functions. Each function consumes the return value of the function that
|
||||||
|
* follows. Composing functions `f()`, `g()`, and `h()` would produce the result
|
||||||
|
* of `f(g(h()))`, only this version uses callbacks to obtain the return values.
|
||||||
|
*
|
||||||
|
* If the last argument to the composed function is not a function, a promise
|
||||||
|
* is returned when you call it.
|
||||||
|
*
|
||||||
|
* Each function is executed with the `this` binding of the composed function.
|
||||||
|
*
|
||||||
|
* @name compose
|
||||||
|
* @static
|
||||||
|
* @memberOf module:ControlFlow
|
||||||
|
* @method
|
||||||
|
* @category Control Flow
|
||||||
|
* @param {...AsyncFunction} functions - the asynchronous functions to compose
|
||||||
|
* @returns {Function} an asynchronous function that is the composed
|
||||||
|
* asynchronous `functions`
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* function add1(n, callback) {
|
||||||
|
* setTimeout(function () {
|
||||||
|
* callback(null, n + 1);
|
||||||
|
* }, 10);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* function mul3(n, callback) {
|
||||||
|
* setTimeout(function () {
|
||||||
|
* callback(null, n * 3);
|
||||||
|
* }, 10);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* var add1mul3 = async.compose(mul3, add1);
|
||||||
|
* add1mul3(4, function (err, result) {
|
||||||
|
* // result now equals 15
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
function compose(...args) {
|
||||||
|
return (0, _seq2.default)(...args.reverse());
|
||||||
|
}
|
||||||
|
module.exports = exports.default;
|
||||||
115
node_modules/async/concat.js
generated
vendored
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _concatLimit = require('./concatLimit.js');
|
||||||
|
|
||||||
|
var _concatLimit2 = _interopRequireDefault(_concatLimit);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies `iteratee` to each item in `coll`, concatenating the results. Returns
|
||||||
|
* the concatenated list. The `iteratee`s are called in parallel, and the
|
||||||
|
* results are concatenated as they return. The results array will be returned in
|
||||||
|
* the original order of `coll` passed to the `iteratee` function.
|
||||||
|
*
|
||||||
|
* @name concat
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @category Collection
|
||||||
|
* @alias flatMap
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {AsyncFunction} iteratee - A function to apply to each item in `coll`,
|
||||||
|
* which should use an array as its result. Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called after all the
|
||||||
|
* `iteratee` functions have finished, or an error occurs. Results is an array
|
||||||
|
* containing the concatenated results of the `iteratee` function. Invoked with
|
||||||
|
* (err, results).
|
||||||
|
* @returns A Promise, if no callback is passed
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* // dir1 is a directory that contains file1.txt, file2.txt
|
||||||
|
* // dir2 is a directory that contains file3.txt, file4.txt
|
||||||
|
* // dir3 is a directory that contains file5.txt
|
||||||
|
* // dir4 does not exist
|
||||||
|
*
|
||||||
|
* let directoryList = ['dir1','dir2','dir3'];
|
||||||
|
* let withMissingDirectoryList = ['dir1','dir2','dir3', 'dir4'];
|
||||||
|
*
|
||||||
|
* // Using callbacks
|
||||||
|
* async.concat(directoryList, fs.readdir, function(err, results) {
|
||||||
|
* if (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* } else {
|
||||||
|
* console.log(results);
|
||||||
|
* // [ 'file1.txt', 'file2.txt', 'file3.txt', 'file4.txt', file5.txt ]
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Error Handling
|
||||||
|
* async.concat(withMissingDirectoryList, fs.readdir, function(err, results) {
|
||||||
|
* if (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* // [ Error: ENOENT: no such file or directory ]
|
||||||
|
* // since dir4 does not exist
|
||||||
|
* } else {
|
||||||
|
* console.log(results);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Using Promises
|
||||||
|
* async.concat(directoryList, fs.readdir)
|
||||||
|
* .then(results => {
|
||||||
|
* console.log(results);
|
||||||
|
* // [ 'file1.txt', 'file2.txt', 'file3.txt', 'file4.txt', file5.txt ]
|
||||||
|
* }).catch(err => {
|
||||||
|
* console.log(err);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Error Handling
|
||||||
|
* async.concat(withMissingDirectoryList, fs.readdir)
|
||||||
|
* .then(results => {
|
||||||
|
* console.log(results);
|
||||||
|
* }).catch(err => {
|
||||||
|
* console.log(err);
|
||||||
|
* // [ Error: ENOENT: no such file or directory ]
|
||||||
|
* // since dir4 does not exist
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Using async/await
|
||||||
|
* async () => {
|
||||||
|
* try {
|
||||||
|
* let results = await async.concat(directoryList, fs.readdir);
|
||||||
|
* console.log(results);
|
||||||
|
* // [ 'file1.txt', 'file2.txt', 'file3.txt', 'file4.txt', file5.txt ]
|
||||||
|
* } catch (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Error Handling
|
||||||
|
* async () => {
|
||||||
|
* try {
|
||||||
|
* let results = await async.concat(withMissingDirectoryList, fs.readdir);
|
||||||
|
* console.log(results);
|
||||||
|
* } catch (err) {
|
||||||
|
* console.log(err);
|
||||||
|
* // [ Error: ENOENT: no such file or directory ]
|
||||||
|
* // since dir4 does not exist
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function concat(coll, iteratee, callback) {
|
||||||
|
return (0, _concatLimit2.default)(coll, Infinity, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(concat, 3);
|
||||||
|
module.exports = exports.default;
|
||||||
60
node_modules/async/concatLimit.js
generated
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||||
|
|
||||||
|
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||||
|
|
||||||
|
var _mapLimit = require('./mapLimit.js');
|
||||||
|
|
||||||
|
var _mapLimit2 = _interopRequireDefault(_mapLimit);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as [`concat`]{@link module:Collections.concat} but runs a maximum of `limit` async operations at a time.
|
||||||
|
*
|
||||||
|
* @name concatLimit
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @see [async.concat]{@link module:Collections.concat}
|
||||||
|
* @category Collection
|
||||||
|
* @alias flatMapLimit
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {number} limit - The maximum number of async operations at a time.
|
||||||
|
* @param {AsyncFunction} iteratee - A function to apply to each item in `coll`,
|
||||||
|
* which should use an array as its result. Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called after all the
|
||||||
|
* `iteratee` functions have finished, or an error occurs. Results is an array
|
||||||
|
* containing the concatenated results of the `iteratee` function. Invoked with
|
||||||
|
* (err, results).
|
||||||
|
* @returns A Promise, if no callback is passed
|
||||||
|
*/
|
||||||
|
function concatLimit(coll, limit, iteratee, callback) {
|
||||||
|
var _iteratee = (0, _wrapAsync2.default)(iteratee);
|
||||||
|
return (0, _mapLimit2.default)(coll, limit, (val, iterCb) => {
|
||||||
|
_iteratee(val, (err, ...args) => {
|
||||||
|
if (err) return iterCb(err);
|
||||||
|
return iterCb(err, args);
|
||||||
|
});
|
||||||
|
}, (err, mapResults) => {
|
||||||
|
var result = [];
|
||||||
|
for (var i = 0; i < mapResults.length; i++) {
|
||||||
|
if (mapResults[i]) {
|
||||||
|
result = result.concat(...mapResults[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(err, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(concatLimit, 4);
|
||||||
|
module.exports = exports.default;
|
||||||
41
node_modules/async/concatSeries.js
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var _concatLimit = require('./concatLimit.js');
|
||||||
|
|
||||||
|
var _concatLimit2 = _interopRequireDefault(_concatLimit);
|
||||||
|
|
||||||
|
var _awaitify = require('./internal/awaitify.js');
|
||||||
|
|
||||||
|
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||||
|
|
||||||
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as [`concat`]{@link module:Collections.concat} but runs only a single async operation at a time.
|
||||||
|
*
|
||||||
|
* @name concatSeries
|
||||||
|
* @static
|
||||||
|
* @memberOf module:Collections
|
||||||
|
* @method
|
||||||
|
* @see [async.concat]{@link module:Collections.concat}
|
||||||
|
* @category Collection
|
||||||
|
* @alias flatMapSeries
|
||||||
|
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||||
|
* @param {AsyncFunction} iteratee - A function to apply to each item in `coll`.
|
||||||
|
* The iteratee should complete with an array an array of results.
|
||||||
|
* Invoked with (item, callback).
|
||||||
|
* @param {Function} [callback] - A callback which is called after all the
|
||||||
|
* `iteratee` functions have finished, or an error occurs. Results is an array
|
||||||
|
* containing the concatenated results of the `iteratee` function. Invoked with
|
||||||
|
* (err, results).
|
||||||
|
* @returns A Promise, if no callback is passed
|
||||||
|
*/
|
||||||
|
function concatSeries(coll, iteratee, callback) {
|
||||||
|
return (0, _concatLimit2.default)(coll, 1, iteratee, callback);
|
||||||
|
}
|
||||||
|
exports.default = (0, _awaitify2.default)(concatSeries, 3);
|
||||||
|
module.exports = exports.default;
|
||||||