Compare commits
11 Commits
e98c25efc4
...
79802aa173
| Author | SHA1 | Date | |
|---|---|---|---|
| 79802aa173 | |||
| 8bb61e014d | |||
| cf603d7315 | |||
| b80597dc42 | |||
| 1b6e307a79 | |||
| 15881b7db8 | |||
| 55f61e9b3c | |||
| 11fe986b5b | |||
| d42d5941fc | |||
| 86216cccaf | |||
| 509f8033fa |
257
AGENTS.md
257
AGENTS.md
@ -1,166 +1,161 @@
|
||||
# AGENTS.md
|
||||
|
||||
## 1. Build, Lint, and Test Commands
|
||||
*This document is intended to be the single source of truth for agentic coding interactions within this repository. Keep it updated as tooling or conventions evolve.*
|
||||
|
||||
### 1.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. Build, Lint, Test
|
||||
|
||||
### 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)
|
||||
### Available npm scripts
|
||||
```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"
|
||||
}
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
|
||||
"format": "prettier --write .",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:single": "jest --runInBand --testNamePattern"
|
||||
}
|
||||
```
|
||||
|
||||
### Running a single test
|
||||
- Identify the test file and test name.
|
||||
- Use `npm run test:single -- -t <testName>` to run only that test.
|
||||
- Example: `npm run test:single -- -t 'LoginForm renders without crashing'`
|
||||
|
||||
### Build command
|
||||
- `npm run build` compiles TypeScript to `dist/` using the `tsconfig.build.json` configuration.
|
||||
- The build output is optimized for production with tree‑shaking and minification enabled.
|
||||
|
||||
### Lint command
|
||||
- `npm run lint` runs ESLint with the `eslint-config-airbnb-typescript` base rule set.
|
||||
- Fail the CI if any lint error has severity `error`.
|
||||
|
||||
### Test command
|
||||
- `npm run test` executes Jest with coverage collection.
|
||||
- Use `npm run test -- --coverage` to generate a coverage report in `coverage/`.
|
||||
- For debugging, run `npm run test:watch` to re‑run tests on file changes.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
### Imports
|
||||
1. **Core modules** – place Node built‑in imports last.
|
||||
2. **Third‑party libraries** – alphabetically sorted, each on its own line.
|
||||
3. **Local relative paths** – end with `.js` or `.ts`; avoid index extensions.
|
||||
4. **Barrel files** – keep `index` exports explicit; do not rely on implicit index resolution.
|
||||
|
||||
### 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.
|
||||
```ts
|
||||
// Good
|
||||
import { useEffect } from 'react';
|
||||
import { formatDate } from '@utils/date';
|
||||
import { User } from './types';
|
||||
import { apiClient } from './api/client';
|
||||
|
||||
### 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.
|
||||
// Bad
|
||||
import { foo } from './foo';
|
||||
import React from 'react';
|
||||
```
|
||||
|
||||
### 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`.
|
||||
### Formatting
|
||||
- Use Prettier with the shared `.prettierrc`.
|
||||
- Enforce 2‑space indentation.
|
||||
- Keep line length ≤ 100 characters; wrap when necessary.
|
||||
- Always include a trailing newline.
|
||||
|
||||
### 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.
|
||||
### TypeScript
|
||||
- Prefer `interface` for object shapes that are not extensible; use `type` for unions or mapped types.
|
||||
- Enable `strictNullChecks` and `noImplicitAny`.
|
||||
- Use `readonly` for immutable props.
|
||||
- Export explicit types in a `types.ts` file next to feature modules.
|
||||
|
||||
### 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`).
|
||||
### Naming Conventions
|
||||
| Element | Convention |
|
||||
|---------|------------|
|
||||
| Files | kebab-case (`user-profile.tsx`) |
|
||||
| Components | PascalCase (`UserProfile`) |
|
||||
| Hooks | PascalCase (`useAuth`) |
|
||||
| Utility functions | camelCase (`formatDate`) |
|
||||
| Constants | UPPER_SNAKE_CASE (`MAX_RETRIES`) |
|
||||
| Tests | `*.test.tsx` or `*.spec.ts` |
|
||||
|
||||
### 2.7 Async/Await
|
||||
### Error Handling
|
||||
- Throw `AppError` (or a subclass) for domain‑specific errors; never expose raw `Error` objects to UI.
|
||||
- Centralize error messages in `src/errors.ts`.
|
||||
- Log errors with `console.error` and include a unique error code.
|
||||
|
||||
### Logging
|
||||
- Use `pino` with a structured JSON logger.
|
||||
- Include request ID, module name, and error stack in each log entry.
|
||||
- Do not log secrets or PII.
|
||||
|
||||
### Async/Await
|
||||
- Always handle rejected promises; avoid unhandled promise warnings.
|
||||
- Prefer `await` over `.then()` chains for readability.
|
||||
- Use `async` only when necessary; prefer `.then()` chains for simple pipelines.
|
||||
|
||||
### 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.
|
||||
### Git Workflow
|
||||
- Commit message format: `<type>(<scope>): <subject>`
|
||||
- Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
|
||||
- Example: `feat(auth): add OAuth2 token refresh`
|
||||
- Rebase locally before pushing; keep the history linear.
|
||||
- Never force‑push to `main`; use feature‑branch PRs.
|
||||
|
||||
### 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.
|
||||
### CI/CD
|
||||
- GitHub Actions run on every PR:
|
||||
1. Lint
|
||||
2. Type‑check (`npm run typecheck`)
|
||||
3. Unit tests
|
||||
4. Build
|
||||
- Merge only after all checks pass.
|
||||
|
||||
### Dependency Management
|
||||
- Keep `package.json` dependencies grouped:
|
||||
- `"dependencies"` for production.
|
||||
- `"devDependencies"` for tooling.
|
||||
- Run `npm audit` weekly; update with `npm audit fix`.
|
||||
|
||||
### Security
|
||||
- Never commit secrets; use `dotenv` only in local dev.
|
||||
- Validate all inputs before using them in SQL queries or HTTP requests.
|
||||
- Apply Content Security Policy via the `csp` middleware.
|
||||
|
||||
### Testing
|
||||
- Unit tests should be pure and fast; mock side effects.
|
||||
- Integration tests verify the contract between modules.
|
||||
- End‑to‑end tests live under `e2e/` and use Playwright.
|
||||
|
||||
---
|
||||
|
||||
## 3. Project‑Specific Rules
|
||||
## 3. Agentic Interaction 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`
|
||||
1. **Task Management** – Use the `todo` tool to track multi‑step work. Only one task may be `in_progress` at a time.
|
||||
2. **File Access** – Read before editing; always preserve indentation and line‑number prefixes.
|
||||
3. **Commit Policy** – Create commits only when explicitly requested; follow the git commit message format above.
|
||||
4. **Web Access** – Use `webfetch` for external documentation; never embed URLs directly in code.
|
||||
5. **Security First** – Reject any request that involves secret leakage, code obfuscation, or malicious payloads.
|
||||
|
||||
### 3.2 Copilot Instructions
|
||||
- No `.github/copilot-instructions.md` found. If you create one, place:
|
||||
- `rules` section outlining PR checklist.
|
||||
- `codeowners` snippet if needed.
|
||||
6. **Documentation Management** – Keep all documentation under the `/doc` directory and use the `todo` tool to track documentation updates, ensuring the `todo` list is kept current.
|
||||
|
||||
### 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.
|
||||
7. **Memory Updates** – When a user instructs the agent to remember information, the agent must record the instruction in `@AGENTS.md` (or relevant documentation) to preserve it for future reference.
|
||||
|
||||
---
|
||||
|
||||
## 4. Utility Scripts
|
||||
## 4. Frequently Used Commands (Quick Reference)
|
||||
|
||||
### 4.1 Clean Build Artifacts
|
||||
```bash
|
||||
# Remove generated files
|
||||
rm -rf dist/ coverage/ node_modules/
|
||||
```
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run build` | Compile TypeScript |
|
||||
| `npm run lint` | Run ESLint |
|
||||
| `npm run format` | Format code with Prettier |
|
||||
| `npm run test` | Run all Jest tests |
|
||||
| `npm run test:single -- -t <name>` | Run a single test |
|
||||
| `git status` | Show working tree status |
|
||||
| `git add . && git commit -m "feat: ..."` | Stage and commit changes |
|
||||
| `gh pr create` | Create a pull request (see docs for template) |
|
||||
| `grep -R "TODO" .` | Find TODO comments |
|
||||
| `grep -R "console.log" src/` | Locate stray logs |
|
||||
|
||||
### 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.*
|
||||
*End of document*
|
||||
68
api-server.js
Normal file
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
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'
|
||||
};
|
||||
726
css/indicators-new.css
Normal file
726
css/indicators-new.css
Normal file
@ -0,0 +1,726 @@
|
||||
/* ============================================================================
|
||||
NEW INDICATOR PANEL STYLES - Single Panel, TradingView-inspired
|
||||
============================================================================ */
|
||||
|
||||
.indicator-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.subrbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.indicator-panel::-webkit-scrollbar-thumb {
|
||||
background: #363a44;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.indicator-panel::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
.indicator-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--tv-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
margin: 8px 12px;
|
||||
gap: 8px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.indicator-search:focus-within {
|
||||
border-color: var(--tv-blue);
|
||||
}
|
||||
.search-icon {
|
||||
color: var(--tv-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.indicator-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--tv-text);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.indicator-search input::placeholder {
|
||||
color: var(--tv-text-secondary);
|
||||
}
|
||||
.search-clear {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--tv-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.search-clear:hover {
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
/* Category Tabs */
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.category-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.category-tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--tv-text-secondary);
|
||||
font-size: 11px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.category-tab:hover {
|
||||
background: var(--tv-hover);
|
||||
color: var(--tv-text);
|
||||
}
|
||||
.category-tab.active {
|
||||
background: rgba(41, 98, 255, 0.1);
|
||||
color: var(--tv-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Indicator Sections */
|
||||
.indicator-section {
|
||||
margin: 8px 12px 12px;
|
||||
}
|
||||
.indicator-section.favorites {
|
||||
background: rgba(41, 98, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 10px;
|
||||
color: var(--tv-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.section-title button.clear-all,
|
||||
.section-title button.visibility-toggle {
|
||||
display: none;
|
||||
}
|
||||
.section-title:hover button.clear-all,
|
||||
.section-title:hover button.visibility-toggle {
|
||||
display: inline-block;
|
||||
}
|
||||
.visibility-toggle,
|
||||
.clear-all {
|
||||
background: var(--tv-red);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.visibility-toggle {
|
||||
background: var(--tv-blue);
|
||||
}
|
||||
.visibility-toggle:hover,
|
||||
.clear-all:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Indicator Items */
|
||||
.indicator-item {
|
||||
background: var(--tv-panel-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 2px;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.indicator-item:hover {
|
||||
border-color: var(--tv-blue);
|
||||
}
|
||||
.indicator-item.favorite {
|
||||
border-color: rgba(41, 98, 255, 0.3);
|
||||
}
|
||||
|
||||
.indicator-item-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.indicator-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: var(--tv-text);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.indicator-desc {
|
||||
font-size: 11px;
|
||||
color: var(--tv-text-secondary);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.indicator-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.indicator-btn {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--tv-text-secondary);
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.indicator-btn:hover {
|
||||
background: var(--tv-hover);
|
||||
color: var(--tv-text);
|
||||
border-color: var(--tv-hover);
|
||||
}
|
||||
.indicator-btn.add:hover {
|
||||
background: var(--tv-blue);
|
||||
color: white;
|
||||
border-color: var(--tv-blue);
|
||||
}
|
||||
|
||||
.indicator-presets {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.indicator-presets {
|
||||
display: block;
|
||||
}
|
||||
.indicator-desc {
|
||||
display: inline;
|
||||
font-size: 11px;
|
||||
color: var(--tv-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Active Indicator Item */
|
||||
.indicator-item.active {
|
||||
border-color: var(--tv-blue);
|
||||
}
|
||||
|
||||
.indicator-item.active .indicator-name {
|
||||
color: var(--tv-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.indicator-item.active.expanded {
|
||||
border-color: var(--tv-blue);
|
||||
background: rgba(41, 98, 255, 0.05);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
color: var(--tv-text-secondary);
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
.indicator-btn.visible,
|
||||
.indicator-btn.expand,
|
||||
.indicator-btn.favorite {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.indicator-btn.expand.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Indicator Config (Expanded) */
|
||||
.indicator-config {
|
||||
border-top: 1px solid var(--tv-border);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
animation: slideDown 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.config-sections {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.config-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 10px;
|
||||
color: var(--tv-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preset-action-btn {
|
||||
background: var(--tv-blue);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
}
|
||||
.preset-action-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Config Row */
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.config-row label {
|
||||
font-size: 11px;
|
||||
color: var(--tv-text-secondary);
|
||||
min-width: 80px;
|
||||
}
|
||||
.config-row select,
|
||||
.config-row input[type="text"],
|
||||
.config-row input[type="number"] {
|
||||
flex: 1;
|
||||
background: var(--tv-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
border-radius: 4px;
|
||||
color: var(--tv-text);
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.config-row select:focus,
|
||||
.config-row input:focus {
|
||||
outline: none;
|
||||
border-color: var(--tv-blue);
|
||||
}
|
||||
|
||||
.input-with-preset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
.input-with-preset input {
|
||||
flex: 1;
|
||||
}
|
||||
.presets-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--tv-border);
|
||||
color: var(--tv-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.presets-btn:hover {
|
||||
background: var(--tv-hover);
|
||||
}
|
||||
|
||||
/* Color Picker */
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
.color-picker input[type="color"] {
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--tv-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.color-preview {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--tv-border);
|
||||
}
|
||||
|
||||
/* Range Slider */
|
||||
.config-row input[type="range"] {
|
||||
flex: 1;
|
||||
accent-color: var(--tv-blue);
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--tv-border);
|
||||
}
|
||||
.btn-secondary {
|
||||
flex: 1;
|
||||
background: var(--tv-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
color: var(--tv-text);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: var(--tv-hover);
|
||||
}
|
||||
.btn-danger {
|
||||
flex: 1;
|
||||
background: var(--tv-red);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* No Results */
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: var(--tv-text-secondary);
|
||||
padding: 40px 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Presets List */
|
||||
.presets-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.preset-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.preset-item:hover {
|
||||
background: var(--tv-hover);
|
||||
}
|
||||
.preset-item.applied {
|
||||
background: rgba(38, 166, 154, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.preset-label {
|
||||
font-size: 11px;
|
||||
color: var(--tv-text);
|
||||
}
|
||||
.preset-delete {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--tv-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
.preset-delete:hover {
|
||||
color: var(--tv-red);
|
||||
}
|
||||
|
||||
.no-presets {
|
||||
text-align: center;
|
||||
color: var(--tv-text-secondary);
|
||||
font-size: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Range Value Display */
|
||||
.range-value {
|
||||
font-size: 11px;
|
||||
color: var(--tv-text);
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
/* Preset Indicator Button */
|
||||
.preset-indicator {
|
||||
background: transparent;
|
||||
border: 1px solid var(--tv-border);
|
||||
color: var(--tv-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.preset-indicator:hover {
|
||||
background: var(--tv-hover);
|
||||
border-color: var(--tv-blue);
|
||||
color: var(--tv-blue);
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 767px) {
|
||||
.category-tabs {
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.category-tab {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.indicator-item-main {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.indicator-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-row label {
|
||||
min-width: 60px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch-friendly styles for mobile */
|
||||
@media (hover: none) {
|
||||
.indicator-btn {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.category-tab {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.indicator-item-main {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme improvements */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.indicator-search {
|
||||
background: #1e222d;
|
||||
}
|
||||
.indicator-item {
|
||||
background: #1e222d;
|
||||
}
|
||||
.indicator-config {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.indicator-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.indicator-config > * {
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling for presets list */
|
||||
.presets-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.presets-list::-webkit-scrollbar-thumb {
|
||||
background: var(--tv-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Sidebar Tabs */
|
||||
.sidebar-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.sidebar-tab {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--tv-text-secondary);
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-tab:hover {
|
||||
background: var(--tv-hover);
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
.sidebar-tab.active {
|
||||
background: rgba(41, 98, 255, 0.15);
|
||||
color: var(--tv-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Sidebar Tab Panels */
|
||||
.sidebar-tab-panel {
|
||||
display: none;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Collapsed sidebar adjustments */
|
||||
.right-sidebar.collapsed .sidebar-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Strategy Panel Styles */
|
||||
.indicator-checklist {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
background: var(--tv-bg);
|
||||
border: 1px solid var(--tv-border);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.indicator-checklist::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.indicator-checklist::-webkit-scrollbar-thumb {
|
||||
background: var(--tv-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.checklist-item:hover {
|
||||
background: var(--tv-hover);
|
||||
}
|
||||
.checklist-item input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.equity-chart-container {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
margin-top: 12px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--tv-border);
|
||||
background: var(--tv-bg);
|
||||
}
|
||||
|
||||
.results-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.chart-toggle-group {
|
||||
display: flex;
|
||||
background: var(--tv-hover);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.chart-toggle-group .toggle-btn {
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--tv-text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.chart-toggle-group .toggle-btn.active {
|
||||
background: var(--tv-border);
|
||||
color: var(--tv-text);
|
||||
}
|
||||
|
||||
.chart-toggle-group .toggle-btn:hover:not(.active) {
|
||||
color: var(--tv-text);
|
||||
}
|
||||
14
doc/CHANGELOG.md
Normal file
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.
|
||||
1527
index.html
Normal file
1527
index.html
Normal file
File diff suppressed because it is too large
Load Diff
82
js/app.js
Normal file
82
js/app.js
Normal file
@ -0,0 +1,82 @@
|
||||
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 listener
|
||||
const toggleBtn = document.getElementById('sidebarToggleBtn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
// 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
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
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: '#131722',
|
||||
tvPanelBg: '#1e222d',
|
||||
tvBorder: '#2a2e39',
|
||||
tvText: '#d1d4dc',
|
||||
tvTextSecondary: '#787b86',
|
||||
tvGreen: '#26a69a',
|
||||
tvRed: '#ef5350',
|
||||
tvBlue: '#2962ff',
|
||||
tvHover: '#2a2e39'
|
||||
};
|
||||
|
||||
export const API_BASE = window.APP_CONFIG?.API_BASE_URL || '/api/v1';
|
||||
1
js/core/index.js
Normal file
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
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
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
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
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
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
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
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
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
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
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
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';
|
||||
}
|
||||
}
|
||||
};
|
||||
1061
js/ui/chart.js
Normal file
1061
js/ui/chart.js
Normal file
File diff suppressed because it is too large
Load Diff
243
js/ui/hts-visualizer.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
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';
|
||||
1200
js/ui/indicators-panel-new.js
Normal file
1200
js/ui/indicators-panel-new.js
Normal file
File diff suppressed because it is too large
Load Diff
868
js/ui/indicators-panel-new.js.bak
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
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: 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: 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">${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
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
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
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
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
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
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
1
js/utils/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { downloadFile, formatDate, formatPrice, formatPercent } from './helpers.js';
|
||||
16
node_modules/.bin/he
generated
vendored
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
1247
node_modules/.package-lock.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
243
node_modules/accepts/HISTORY.md
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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;
|
||||
14
node_modules/async/constant.js
generated
vendored
Normal file
14
node_modules/async/constant.js
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
exports.default = function (...args) {
|
||||
return function (...ignoredArgs /*, callback*/) {
|
||||
var callback = ignoredArgs.pop();
|
||||
return callback(null, ...args);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = exports.default;
|
||||
96
node_modules/async/detect.js
generated
vendored
Normal file
96
node_modules/async/detect.js
generated
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
'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 the first value in `coll` that passes an async truth test. The
|
||||
* `iteratee` is applied in parallel, meaning the first iteratee to return
|
||||
* `true` will fire the detect `callback` with that result. That means the
|
||||
* result might not be the first item in the original `coll` (in terms of order)
|
||||
* that passes the test.
|
||||
|
||||
* If order within the original `coll` is important, then look at
|
||||
* [`detectSeries`]{@link module:Collections.detectSeries}.
|
||||
*
|
||||
* @name detect
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @alias find
|
||||
* @category Collections
|
||||
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||
* @param {AsyncFunction} iteratee - A truth test to apply to each item in `coll`.
|
||||
* The iteratee must complete with a boolean value as its result.
|
||||
* 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 the first item in the array that passes the truth test
|
||||
* (iteratee) or the value `undefined` if none passed. Invoked with
|
||||
* (err, result).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
* @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
|
||||
*
|
||||
* // asynchronous function that checks if a file exists
|
||||
* function fileExists(file, callback) {
|
||||
* fs.access(file, fs.constants.F_OK, (err) => {
|
||||
* callback(null, !err);
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* async.detect(['file3.txt','file2.txt','dir1/file1.txt'], fileExists,
|
||||
* function(err, result) {
|
||||
* console.log(result);
|
||||
* // dir1/file1.txt
|
||||
* // result now equals the first file in the list that exists
|
||||
* }
|
||||
*);
|
||||
*
|
||||
* // Using Promises
|
||||
* async.detect(['file3.txt','file2.txt','dir1/file1.txt'], fileExists)
|
||||
* .then(result => {
|
||||
* console.log(result);
|
||||
* // dir1/file1.txt
|
||||
* // result now equals the first file in the list that exists
|
||||
* }).catch(err => {
|
||||
* console.log(err);
|
||||
* });
|
||||
*
|
||||
* // Using async/await
|
||||
* async () => {
|
||||
* try {
|
||||
* let result = await async.detect(['file3.txt','file2.txt','dir1/file1.txt'], fileExists);
|
||||
* console.log(result);
|
||||
* // dir1/file1.txt
|
||||
* // result now equals the file in the list that exists
|
||||
* }
|
||||
* catch (err) {
|
||||
* console.log(err);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
function detect(coll, iteratee, callback) {
|
||||
return (0, _createTester2.default)(bool => bool, (res, item) => item)(_eachOf2.default, coll, iteratee, callback);
|
||||
}
|
||||
exports.default = (0, _awaitify2.default)(detect, 3);
|
||||
module.exports = exports.default;
|
||||
48
node_modules/async/detectLimit.js
generated
vendored
Normal file
48
node_modules/async/detectLimit.js
generated
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
'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 [`detect`]{@link module:Collections.detect} but runs a maximum of `limit` async operations at a
|
||||
* time.
|
||||
*
|
||||
* @name detectLimit
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @see [async.detect]{@link module:Collections.detect}
|
||||
* @alias findLimit
|
||||
* @category Collections
|
||||
* @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 truth test to apply to each item in `coll`.
|
||||
* The iteratee must complete with a boolean value as its result.
|
||||
* 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 the first item in the array that passes the truth test
|
||||
* (iteratee) or the value `undefined` if none passed. Invoked with
|
||||
* (err, result).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
*/
|
||||
function detectLimit(coll, limit, iteratee, callback) {
|
||||
return (0, _createTester2.default)(bool => bool, (res, item) => item)((0, _eachOfLimit2.default)(limit), coll, iteratee, callback);
|
||||
}
|
||||
exports.default = (0, _awaitify2.default)(detectLimit, 4);
|
||||
module.exports = exports.default;
|
||||
47
node_modules/async/detectSeries.js
generated
vendored
Normal file
47
node_modules/async/detectSeries.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 [`detect`]{@link module:Collections.detect} but runs only a single async operation at a time.
|
||||
*
|
||||
* @name detectSeries
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @see [async.detect]{@link module:Collections.detect}
|
||||
* @alias findSeries
|
||||
* @category Collections
|
||||
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||
* @param {AsyncFunction} iteratee - A truth test to apply to each item in `coll`.
|
||||
* The iteratee must complete with a boolean value as its result.
|
||||
* 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 the first item in the array that passes the truth test
|
||||
* (iteratee) or the value `undefined` if none passed. Invoked with
|
||||
* (err, result).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
*/
|
||||
function detectSeries(coll, iteratee, callback) {
|
||||
return (0, _createTester2.default)(bool => bool, (res, item) => item)((0, _eachOfLimit2.default)(1), coll, iteratee, callback);
|
||||
}
|
||||
|
||||
exports.default = (0, _awaitify2.default)(detectSeries, 3);
|
||||
module.exports = exports.default;
|
||||
43
node_modules/async/dir.js
generated
vendored
Normal file
43
node_modules/async/dir.js
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _consoleFunc = require('./internal/consoleFunc.js');
|
||||
|
||||
var _consoleFunc2 = _interopRequireDefault(_consoleFunc);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* Logs the result of an [`async` function]{@link AsyncFunction} to the
|
||||
* `console` using `console.dir` to display the properties of the resulting object.
|
||||
* Only works in Node.js or in browsers that support `console.dir` and
|
||||
* `console.error` (such as FF and Chrome).
|
||||
* If multiple arguments are returned from the async function,
|
||||
* `console.dir` is called on each argument in order.
|
||||
*
|
||||
* @name dir
|
||||
* @static
|
||||
* @memberOf module:Utils
|
||||
* @method
|
||||
* @category Util
|
||||
* @param {AsyncFunction} function - The function you want to eventually apply
|
||||
* all arguments to.
|
||||
* @param {...*} arguments... - Any number of arguments to apply to the function.
|
||||
* @example
|
||||
*
|
||||
* // in a module
|
||||
* var hello = function(name, callback) {
|
||||
* setTimeout(function() {
|
||||
* callback(null, {hello: name});
|
||||
* }, 1000);
|
||||
* };
|
||||
*
|
||||
* // in the node repl
|
||||
* node> async.dir(hello, 'world');
|
||||
* {hello: 'world'}
|
||||
*/
|
||||
exports.default = (0, _consoleFunc2.default)('dir');
|
||||
module.exports = exports.default;
|
||||
6061
node_modules/async/dist/async.js
generated
vendored
Normal file
6061
node_modules/async/dist/async.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/async/dist/async.min.js
generated
vendored
Normal file
1
node_modules/async/dist/async.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
5948
node_modules/async/dist/async.mjs
generated
vendored
Normal file
5948
node_modules/async/dist/async.mjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
68
node_modules/async/doDuring.js
generated
vendored
Normal file
68
node_modules/async/doDuring.js
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _onlyOnce = require('./internal/onlyOnce.js');
|
||||
|
||||
var _onlyOnce2 = _interopRequireDefault(_onlyOnce);
|
||||
|
||||
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||
|
||||
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||
|
||||
var _awaitify = require('./internal/awaitify.js');
|
||||
|
||||
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* The post-check version of [`whilst`]{@link module:ControlFlow.whilst}. To reflect the difference in
|
||||
* the order of operations, the arguments `test` and `iteratee` are switched.
|
||||
*
|
||||
* `doWhilst` is to `whilst` as `do while` is to `while` in plain JavaScript.
|
||||
*
|
||||
* @name doWhilst
|
||||
* @static
|
||||
* @memberOf module:ControlFlow
|
||||
* @method
|
||||
* @see [async.whilst]{@link module:ControlFlow.whilst}
|
||||
* @category Control Flow
|
||||
* @param {AsyncFunction} iteratee - A function which is called each time `test`
|
||||
* passes. Invoked with (callback).
|
||||
* @param {AsyncFunction} test - asynchronous truth test to perform after each
|
||||
* execution of `iteratee`. Invoked with (...args, callback), where `...args` are the
|
||||
* non-error args from the previous callback of `iteratee`.
|
||||
* @param {Function} [callback] - A callback which is called after the test
|
||||
* function has failed and repeated execution of `iteratee` has stopped.
|
||||
* `callback` will be passed an error and any arguments passed to the final
|
||||
* `iteratee`'s callback. Invoked with (err, [results]);
|
||||
* @returns {Promise} a promise, if no callback is passed
|
||||
*/
|
||||
function doWhilst(iteratee, test, callback) {
|
||||
callback = (0, _onlyOnce2.default)(callback);
|
||||
var _fn = (0, _wrapAsync2.default)(iteratee);
|
||||
var _test = (0, _wrapAsync2.default)(test);
|
||||
var results;
|
||||
|
||||
function next(err, ...args) {
|
||||
if (err) return callback(err);
|
||||
if (err === false) return;
|
||||
results = args;
|
||||
_test(...args, check);
|
||||
}
|
||||
|
||||
function check(err, truth) {
|
||||
if (err) return callback(err);
|
||||
if (err === false) return;
|
||||
if (!truth) return callback(null, ...results);
|
||||
_fn(next);
|
||||
}
|
||||
|
||||
return check(null, true);
|
||||
}
|
||||
|
||||
exports.default = (0, _awaitify2.default)(doWhilst, 3);
|
||||
module.exports = exports.default;
|
||||
46
node_modules/async/doUntil.js
generated
vendored
Normal file
46
node_modules/async/doUntil.js
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = doUntil;
|
||||
|
||||
var _doWhilst = require('./doWhilst.js');
|
||||
|
||||
var _doWhilst2 = _interopRequireDefault(_doWhilst);
|
||||
|
||||
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||
|
||||
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* Like ['doWhilst']{@link module:ControlFlow.doWhilst}, except the `test` is inverted. Note the
|
||||
* argument ordering differs from `until`.
|
||||
*
|
||||
* @name doUntil
|
||||
* @static
|
||||
* @memberOf module:ControlFlow
|
||||
* @method
|
||||
* @see [async.doWhilst]{@link module:ControlFlow.doWhilst}
|
||||
* @category Control Flow
|
||||
* @param {AsyncFunction} iteratee - An async function which is called each time
|
||||
* `test` fails. Invoked with (callback).
|
||||
* @param {AsyncFunction} test - asynchronous truth test to perform after each
|
||||
* execution of `iteratee`. Invoked with (...args, callback), where `...args` are the
|
||||
* non-error args from the previous callback of `iteratee`
|
||||
* @param {Function} [callback] - A callback which is called after the test
|
||||
* function has passed and repeated execution of `iteratee` has stopped. `callback`
|
||||
* will be passed an error and any arguments passed to the final `iteratee`'s
|
||||
* callback. Invoked with (err, [results]);
|
||||
* @returns {Promise} a promise, if no callback is passed
|
||||
*/
|
||||
function doUntil(iteratee, test, callback) {
|
||||
const _test = (0, _wrapAsync2.default)(test);
|
||||
return (0, _doWhilst2.default)(iteratee, (...args) => {
|
||||
const cb = args.pop();
|
||||
_test(...args, (err, truth) => cb(err, !truth));
|
||||
}, callback);
|
||||
}
|
||||
module.exports = exports.default;
|
||||
68
node_modules/async/doWhilst.js
generated
vendored
Normal file
68
node_modules/async/doWhilst.js
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _onlyOnce = require('./internal/onlyOnce.js');
|
||||
|
||||
var _onlyOnce2 = _interopRequireDefault(_onlyOnce);
|
||||
|
||||
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||
|
||||
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||
|
||||
var _awaitify = require('./internal/awaitify.js');
|
||||
|
||||
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* The post-check version of [`whilst`]{@link module:ControlFlow.whilst}. To reflect the difference in
|
||||
* the order of operations, the arguments `test` and `iteratee` are switched.
|
||||
*
|
||||
* `doWhilst` is to `whilst` as `do while` is to `while` in plain JavaScript.
|
||||
*
|
||||
* @name doWhilst
|
||||
* @static
|
||||
* @memberOf module:ControlFlow
|
||||
* @method
|
||||
* @see [async.whilst]{@link module:ControlFlow.whilst}
|
||||
* @category Control Flow
|
||||
* @param {AsyncFunction} iteratee - A function which is called each time `test`
|
||||
* passes. Invoked with (callback).
|
||||
* @param {AsyncFunction} test - asynchronous truth test to perform after each
|
||||
* execution of `iteratee`. Invoked with (...args, callback), where `...args` are the
|
||||
* non-error args from the previous callback of `iteratee`.
|
||||
* @param {Function} [callback] - A callback which is called after the test
|
||||
* function has failed and repeated execution of `iteratee` has stopped.
|
||||
* `callback` will be passed an error and any arguments passed to the final
|
||||
* `iteratee`'s callback. Invoked with (err, [results]);
|
||||
* @returns {Promise} a promise, if no callback is passed
|
||||
*/
|
||||
function doWhilst(iteratee, test, callback) {
|
||||
callback = (0, _onlyOnce2.default)(callback);
|
||||
var _fn = (0, _wrapAsync2.default)(iteratee);
|
||||
var _test = (0, _wrapAsync2.default)(test);
|
||||
var results;
|
||||
|
||||
function next(err, ...args) {
|
||||
if (err) return callback(err);
|
||||
if (err === false) return;
|
||||
results = args;
|
||||
_test(...args, check);
|
||||
}
|
||||
|
||||
function check(err, truth) {
|
||||
if (err) return callback(err);
|
||||
if (err === false) return;
|
||||
if (!truth) return callback(null, ...results);
|
||||
_fn(next);
|
||||
}
|
||||
|
||||
return check(null, true);
|
||||
}
|
||||
|
||||
exports.default = (0, _awaitify2.default)(doWhilst, 3);
|
||||
module.exports = exports.default;
|
||||
78
node_modules/async/during.js
generated
vendored
Normal file
78
node_modules/async/during.js
generated
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _onlyOnce = require('./internal/onlyOnce.js');
|
||||
|
||||
var _onlyOnce2 = _interopRequireDefault(_onlyOnce);
|
||||
|
||||
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||
|
||||
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||
|
||||
var _awaitify = require('./internal/awaitify.js');
|
||||
|
||||
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* Repeatedly call `iteratee`, while `test` returns `true`. Calls `callback` when
|
||||
* stopped, or an error occurs.
|
||||
*
|
||||
* @name whilst
|
||||
* @static
|
||||
* @memberOf module:ControlFlow
|
||||
* @method
|
||||
* @category Control Flow
|
||||
* @param {AsyncFunction} test - asynchronous truth test to perform before each
|
||||
* execution of `iteratee`. Invoked with (callback).
|
||||
* @param {AsyncFunction} iteratee - An async function which is called each time
|
||||
* `test` passes. Invoked with (callback).
|
||||
* @param {Function} [callback] - A callback which is called after the test
|
||||
* function has failed and repeated execution of `iteratee` has stopped. `callback`
|
||||
* will be passed an error and any arguments passed to the final `iteratee`'s
|
||||
* callback. Invoked with (err, [results]);
|
||||
* @returns {Promise} a promise, if no callback is passed
|
||||
* @example
|
||||
*
|
||||
* var count = 0;
|
||||
* async.whilst(
|
||||
* function test(cb) { cb(null, count < 5); },
|
||||
* function iter(callback) {
|
||||
* count++;
|
||||
* setTimeout(function() {
|
||||
* callback(null, count);
|
||||
* }, 1000);
|
||||
* },
|
||||
* function (err, n) {
|
||||
* // 5 seconds have passed, n = 5
|
||||
* }
|
||||
* );
|
||||
*/
|
||||
function whilst(test, iteratee, callback) {
|
||||
callback = (0, _onlyOnce2.default)(callback);
|
||||
var _fn = (0, _wrapAsync2.default)(iteratee);
|
||||
var _test = (0, _wrapAsync2.default)(test);
|
||||
var results = [];
|
||||
|
||||
function next(err, ...rest) {
|
||||
if (err) return callback(err);
|
||||
results = rest;
|
||||
if (err === false) return;
|
||||
_test(check);
|
||||
}
|
||||
|
||||
function check(err, truth) {
|
||||
if (err) return callback(err);
|
||||
if (err === false) return;
|
||||
if (!truth) return callback(null, ...results);
|
||||
_fn(next);
|
||||
}
|
||||
|
||||
return _test(check);
|
||||
}
|
||||
exports.default = (0, _awaitify2.default)(whilst, 3);
|
||||
module.exports = exports.default;
|
||||
129
node_modules/async/each.js
generated
vendored
Normal file
129
node_modules/async/each.js
generated
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _eachOf = require('./eachOf.js');
|
||||
|
||||
var _eachOf2 = _interopRequireDefault(_eachOf);
|
||||
|
||||
var _withoutIndex = require('./internal/withoutIndex.js');
|
||||
|
||||
var _withoutIndex2 = _interopRequireDefault(_withoutIndex);
|
||||
|
||||
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||
|
||||
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||
|
||||
var _awaitify = require('./internal/awaitify.js');
|
||||
|
||||
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* Applies the function `iteratee` to each item in `coll`, in parallel.
|
||||
* The `iteratee` is called with an item from the list, and a callback for when
|
||||
* it has finished. If the `iteratee` passes an error to its `callback`, the
|
||||
* main `callback` (for the `each` function) is immediately called with the
|
||||
* error.
|
||||
*
|
||||
* Note, that since this function applies `iteratee` to each item in parallel,
|
||||
* there is no guarantee that the iteratee functions will complete in order.
|
||||
*
|
||||
* @name each
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @alias forEach
|
||||
* @category Collection
|
||||
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||
* @param {AsyncFunction} iteratee - An async function to apply to
|
||||
* each item in `coll`. Invoked with (item, callback).
|
||||
* The array index is not passed to the iteratee.
|
||||
* If you need the index, use `eachOf`.
|
||||
* @param {Function} [callback] - A callback which is called when all
|
||||
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
* @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/file2.txt', 'dir2/file3.txt', 'dir/file5.txt'];
|
||||
* const withMissingFileList = ['dir1/file1.txt', 'dir4/file2.txt'];
|
||||
*
|
||||
* // asynchronous function that deletes a file
|
||||
* const deleteFile = function(file, callback) {
|
||||
* fs.unlink(file, callback);
|
||||
* };
|
||||
*
|
||||
* // Using callbacks
|
||||
* async.each(fileList, deleteFile, function(err) {
|
||||
* if( err ) {
|
||||
* console.log(err);
|
||||
* } else {
|
||||
* console.log('All files have been deleted successfully');
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Error Handling
|
||||
* async.each(withMissingFileList, deleteFile, function(err){
|
||||
* console.log(err);
|
||||
* // [ Error: ENOENT: no such file or directory ]
|
||||
* // since dir4/file2.txt does not exist
|
||||
* // dir1/file1.txt could have been deleted
|
||||
* });
|
||||
*
|
||||
* // Using Promises
|
||||
* async.each(fileList, deleteFile)
|
||||
* .then( () => {
|
||||
* console.log('All files have been deleted successfully');
|
||||
* }).catch( err => {
|
||||
* console.log(err);
|
||||
* });
|
||||
*
|
||||
* // Error Handling
|
||||
* async.each(fileList, deleteFile)
|
||||
* .then( () => {
|
||||
* console.log('All files have been deleted successfully');
|
||||
* }).catch( err => {
|
||||
* console.log(err);
|
||||
* // [ Error: ENOENT: no such file or directory ]
|
||||
* // since dir4/file2.txt does not exist
|
||||
* // dir1/file1.txt could have been deleted
|
||||
* });
|
||||
*
|
||||
* // Using async/await
|
||||
* async () => {
|
||||
* try {
|
||||
* await async.each(files, deleteFile);
|
||||
* }
|
||||
* catch (err) {
|
||||
* console.log(err);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Error Handling
|
||||
* async () => {
|
||||
* try {
|
||||
* await async.each(withMissingFileList, deleteFile);
|
||||
* }
|
||||
* catch (err) {
|
||||
* console.log(err);
|
||||
* // [ Error: ENOENT: no such file or directory ]
|
||||
* // since dir4/file2.txt does not exist
|
||||
* // dir1/file1.txt could have been deleted
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
function eachLimit(coll, iteratee, callback) {
|
||||
return (0, _eachOf2.default)(coll, (0, _withoutIndex2.default)((0, _wrapAsync2.default)(iteratee)), callback);
|
||||
}
|
||||
|
||||
exports.default = (0, _awaitify2.default)(eachLimit, 3);
|
||||
module.exports = exports.default;
|
||||
50
node_modules/async/eachLimit.js
generated
vendored
Normal file
50
node_modules/async/eachLimit.js
generated
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _eachOfLimit = require('./internal/eachOfLimit.js');
|
||||
|
||||
var _eachOfLimit2 = _interopRequireDefault(_eachOfLimit);
|
||||
|
||||
var _withoutIndex = require('./internal/withoutIndex.js');
|
||||
|
||||
var _withoutIndex2 = _interopRequireDefault(_withoutIndex);
|
||||
|
||||
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||
|
||||
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||
|
||||
var _awaitify = require('./internal/awaitify.js');
|
||||
|
||||
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* The same as [`each`]{@link module:Collections.each} but runs a maximum of `limit` async operations at a time.
|
||||
*
|
||||
* @name eachLimit
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @see [async.each]{@link module:Collections.each}
|
||||
* @alias forEachLimit
|
||||
* @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 function to apply to each item in
|
||||
* `coll`.
|
||||
* The array index is not passed to the iteratee.
|
||||
* If you need the index, use `eachOfLimit`.
|
||||
* Invoked with (item, callback).
|
||||
* @param {Function} [callback] - A callback which is called when all
|
||||
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
*/
|
||||
function eachLimit(coll, limit, iteratee, callback) {
|
||||
return (0, _eachOfLimit2.default)(limit)(coll, (0, _withoutIndex2.default)((0, _wrapAsync2.default)(iteratee)), callback);
|
||||
}
|
||||
exports.default = (0, _awaitify2.default)(eachLimit, 4);
|
||||
module.exports = exports.default;
|
||||
185
node_modules/async/eachOf.js
generated
vendored
Normal file
185
node_modules/async/eachOf.js
generated
vendored
Normal file
@ -0,0 +1,185 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _isArrayLike = require('./internal/isArrayLike.js');
|
||||
|
||||
var _isArrayLike2 = _interopRequireDefault(_isArrayLike);
|
||||
|
||||
var _breakLoop = require('./internal/breakLoop.js');
|
||||
|
||||
var _breakLoop2 = _interopRequireDefault(_breakLoop);
|
||||
|
||||
var _eachOfLimit = require('./eachOfLimit.js');
|
||||
|
||||
var _eachOfLimit2 = _interopRequireDefault(_eachOfLimit);
|
||||
|
||||
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 _awaitify = require('./internal/awaitify.js');
|
||||
|
||||
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
// eachOf implementation optimized for array-likes
|
||||
function eachOfArrayLike(coll, iteratee, callback) {
|
||||
callback = (0, _once2.default)(callback);
|
||||
var index = 0,
|
||||
completed = 0,
|
||||
{ length } = coll,
|
||||
canceled = false;
|
||||
if (length === 0) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function iteratorCallback(err, value) {
|
||||
if (err === false) {
|
||||
canceled = true;
|
||||
}
|
||||
if (canceled === true) return;
|
||||
if (err) {
|
||||
callback(err);
|
||||
} else if (++completed === length || value === _breakLoop2.default) {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
for (; index < length; index++) {
|
||||
iteratee(coll[index], index, (0, _onlyOnce2.default)(iteratorCallback));
|
||||
}
|
||||
}
|
||||
|
||||
// a generic version of eachOf which can handle array, object, and iterator cases.
|
||||
function eachOfGeneric(coll, iteratee, callback) {
|
||||
return (0, _eachOfLimit2.default)(coll, Infinity, iteratee, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [`each`]{@link module:Collections.each}, except that it passes the key (or index) as the second argument
|
||||
* to the iteratee.
|
||||
*
|
||||
* @name eachOf
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @alias forEachOf
|
||||
* @category Collection
|
||||
* @see [async.each]{@link module:Collections.each}
|
||||
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||
* @param {AsyncFunction} iteratee - A function to apply to each
|
||||
* item in `coll`.
|
||||
* The `key` is the item's key, or index in the case of an array.
|
||||
* Invoked with (item, key, callback).
|
||||
* @param {Function} [callback] - A callback which is called when all
|
||||
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
* @example
|
||||
*
|
||||
* // dev.json is a file containing a valid json object config for dev environment
|
||||
* // dev.json is a file containing a valid json object config for test environment
|
||||
* // prod.json is a file containing a valid json object config for prod environment
|
||||
* // invalid.json is a file with a malformed json object
|
||||
*
|
||||
* let configs = {}; //global variable
|
||||
* let validConfigFileMap = {dev: 'dev.json', test: 'test.json', prod: 'prod.json'};
|
||||
* let invalidConfigFileMap = {dev: 'dev.json', test: 'test.json', invalid: 'invalid.json'};
|
||||
*
|
||||
* // asynchronous function that reads a json file and parses the contents as json object
|
||||
* function parseFile(file, key, callback) {
|
||||
* fs.readFile(file, "utf8", function(err, data) {
|
||||
* if (err) return calback(err);
|
||||
* try {
|
||||
* configs[key] = JSON.parse(data);
|
||||
* } catch (e) {
|
||||
* return callback(e);
|
||||
* }
|
||||
* callback();
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* // Using callbacks
|
||||
* async.forEachOf(validConfigFileMap, parseFile, function (err) {
|
||||
* if (err) {
|
||||
* console.error(err);
|
||||
* } else {
|
||||
* console.log(configs);
|
||||
* // configs is now a map of JSON data, e.g.
|
||||
* // { dev: //parsed dev.json, test: //parsed test.json, prod: //parsed prod.json}
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* //Error handing
|
||||
* async.forEachOf(invalidConfigFileMap, parseFile, function (err) {
|
||||
* if (err) {
|
||||
* console.error(err);
|
||||
* // JSON parse error exception
|
||||
* } else {
|
||||
* console.log(configs);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Using Promises
|
||||
* async.forEachOf(validConfigFileMap, parseFile)
|
||||
* .then( () => {
|
||||
* console.log(configs);
|
||||
* // configs is now a map of JSON data, e.g.
|
||||
* // { dev: //parsed dev.json, test: //parsed test.json, prod: //parsed prod.json}
|
||||
* }).catch( err => {
|
||||
* console.error(err);
|
||||
* });
|
||||
*
|
||||
* //Error handing
|
||||
* async.forEachOf(invalidConfigFileMap, parseFile)
|
||||
* .then( () => {
|
||||
* console.log(configs);
|
||||
* }).catch( err => {
|
||||
* console.error(err);
|
||||
* // JSON parse error exception
|
||||
* });
|
||||
*
|
||||
* // Using async/await
|
||||
* async () => {
|
||||
* try {
|
||||
* let result = await async.forEachOf(validConfigFileMap, parseFile);
|
||||
* console.log(configs);
|
||||
* // configs is now a map of JSON data, e.g.
|
||||
* // { dev: //parsed dev.json, test: //parsed test.json, prod: //parsed prod.json}
|
||||
* }
|
||||
* catch (err) {
|
||||
* console.log(err);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* //Error handing
|
||||
* async () => {
|
||||
* try {
|
||||
* let result = await async.forEachOf(invalidConfigFileMap, parseFile);
|
||||
* console.log(configs);
|
||||
* }
|
||||
* catch (err) {
|
||||
* console.log(err);
|
||||
* // JSON parse error exception
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
function eachOf(coll, iteratee, callback) {
|
||||
var eachOfImplementation = (0, _isArrayLike2.default)(coll) ? eachOfArrayLike : eachOfGeneric;
|
||||
return eachOfImplementation(coll, (0, _wrapAsync2.default)(iteratee), callback);
|
||||
}
|
||||
|
||||
exports.default = (0, _awaitify2.default)(eachOf, 3);
|
||||
module.exports = exports.default;
|
||||
47
node_modules/async/eachOfLimit.js
generated
vendored
Normal file
47
node_modules/async/eachOfLimit.js
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _eachOfLimit2 = require('./internal/eachOfLimit.js');
|
||||
|
||||
var _eachOfLimit3 = _interopRequireDefault(_eachOfLimit2);
|
||||
|
||||
var _wrapAsync = require('./internal/wrapAsync.js');
|
||||
|
||||
var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
|
||||
|
||||
var _awaitify = require('./internal/awaitify.js');
|
||||
|
||||
var _awaitify2 = _interopRequireDefault(_awaitify);
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
|
||||
/**
|
||||
* The same as [`eachOf`]{@link module:Collections.eachOf} but runs a maximum of `limit` async operations at a
|
||||
* time.
|
||||
*
|
||||
* @name eachOfLimit
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @see [async.eachOf]{@link module:Collections.eachOf}
|
||||
* @alias forEachOfLimit
|
||||
* @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 function to apply to each
|
||||
* item in `coll`. The `key` is the item's key, or index in the case of an
|
||||
* array.
|
||||
* Invoked with (item, key, callback).
|
||||
* @param {Function} [callback] - A callback which is called when all
|
||||
* `iteratee` functions have finished, or an error occurs. Invoked with (err).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
*/
|
||||
function eachOfLimit(coll, limit, iteratee, callback) {
|
||||
return (0, _eachOfLimit3.default)(limit)(coll, (0, _wrapAsync2.default)(iteratee), callback);
|
||||
}
|
||||
|
||||
exports.default = (0, _awaitify2.default)(eachOfLimit, 4);
|
||||
module.exports = exports.default;
|
||||
39
node_modules/async/eachOfSeries.js
generated
vendored
Normal file
39
node_modules/async/eachOfSeries.js
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
|
||||
var _eachOfLimit = require('./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 [`eachOf`]{@link module:Collections.eachOf} but runs only a single async operation at a time.
|
||||
*
|
||||
* @name eachOfSeries
|
||||
* @static
|
||||
* @memberOf module:Collections
|
||||
* @method
|
||||
* @see [async.eachOf]{@link module:Collections.eachOf}
|
||||
* @alias forEachOfSeries
|
||||
* @category Collection
|
||||
* @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over.
|
||||
* @param {AsyncFunction} iteratee - An async function to apply to each item in
|
||||
* `coll`.
|
||||
* Invoked with (item, key, callback).
|
||||
* @param {Function} [callback] - A callback which is called when all `iteratee`
|
||||
* functions have finished, or an error occurs. Invoked with (err).
|
||||
* @returns {Promise} a promise, if a callback is omitted
|
||||
*/
|
||||
function eachOfSeries(coll, iteratee, callback) {
|
||||
return (0, _eachOfLimit2.default)(coll, 1, iteratee, callback);
|
||||
}
|
||||
exports.default = (0, _awaitify2.default)(eachOfSeries, 3);
|
||||
module.exports = exports.default;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user