diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0be1a31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +binance_rust + +/kiro.json + +.claude +.devcontainer +storage +tradingview-demo + +backend/.env + +/frontend/.env \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b198307 --- /dev/null +++ b/README.md @@ -0,0 +1,401 @@ +# 📊 View — Crypto chart Platform + +[🇨🇳 中文版 README](README_CN.md) +A self-hosted, real-time cryptocurrency data collection and visualization platform for **Binance USDS-M Futures**. Combines a high-performance Rust backend with a professional charting frontend to deliver institutional-grade market data tooling. + +This project is primarily built for my own use, and I don't have much bandwidth to maintain it. I'm also not a frontend developer, so the frontend code may not follow best practices. That said, pull requests and issues are very welcome — feel free to open an Issue or submit a Pull Request! + +As you can see, this is not a simple one-click-to-deploy project, nor is it a beginner-friendly toy. I simply don't have the time or ability to simplify it further. The easiest way to deploy is to let **Claude Opus 4.6** study the entire project and have it guide you through the setup. + +If deploying the backend is too difficult, you can temporarily use my public backend API endpoint: `api-view.cathiefish.org`. This way you only need to deploy the frontend to Cloudflare Pages. Note that this endpoint only guarantees data for **BTCUSDT, ETHUSDT, SOLUSDT, BNBUSDT, XRPUSDT, SUIUSDT**. See the [Deployment](#-deployment) section for how to configure this. + +![tv-1](https://img.cathiefish.art/ns/tv-1.png) +![tv-2](https://img.cathiefish.art/ns/tv-2.png) +![tv-3](https://img.cathiefish.art/ns/tv-3.png) + +## ✨ Features + +### Data Collection & Storage +- [x] **Real-time WebSocket streaming** — Live 1-minute K-line data from Binance combined streams, with auto-reconnect and gap backfill +- [x] **3-tier historical sync** — Monthly ZIP → Daily ZIP → REST API, for fastest possible backfill from [Binance Data Archive](https://data.binance.vision/) +- [x] **TimescaleDB time-series storage** — Hypertable-optimized with `time_bucket` aggregation across 8 timeframes (1m, 5m, 15m, 1h, 4h, 1D, 1W, 1M) +- [x] **Proxy pool support** — Up to 100 concurrent proxy clients for high-throughput parallel downloads +- [ ] **Binance Spot support** — Spot market data collection, using `.P` suffix to distinguish Futures from Spot symbols +- [ ] **MCP support** — Model Context Protocol integration + +### Charting & Visualization +- [x] **Professional charting engine** — Full indicator and drawing tool support +- [x] **Custom indicators** — Net Volume (NV-C) and Cumulative Volume Delta (CVD-C) +- [x] **Multi-chart layouts** — Single, vertical split, horizontal split, and 1L+2R layouts with draggable dividers +- [x] **Canvas persistence** — Named drawing canvases per symbol, with auto-save every 5 minutes +- [x] **Dark & Light themes** — Full theme toggle synced across widget and UI + +### Watchlist & Interaction +- [x] **Live watchlist sidebar** — Real-time price, 24h change tracking, drag-and-drop reordering +- [x] **Multiple custom lists** — Create and manage separate watchlists +- [ ] **Frontend symbol management** — Add/remove tracked symbols from the frontend UI (currently `TRACKED_SYMBOL` is only controlled via the backend `.env` file) + +### Security & Deployment +- [x] **Google OAuth protection** — Email whitelist access control with 144-hour session persistence +- [x] **Docker ready** — Multi-stage builds, Docker Compose with shared network +- [ ] **Google Firebase integration** — Connect backend API via Firebase for managed authentication and hosting + +## 📁 Project Structure + +``` +view/ +├── backend/ # Rust data engine & API server +│ ├── src/ +│ │ ├── main.rs # Axum HTTP server bootstrap +│ │ ├── binance_collector.rs # WebSocket real-time collection + REST sync +│ │ ├── historical_downloader.rs # Binance data archive (ZIP) downloader +│ │ ├── database.rs # TimescaleDB operations & aggregation +│ │ ├── scheduler.rs # Task coordination & collector lifecycle +│ │ ├── klinechart.rs # KlineChart REST API handlers +│ │ ├── tradingview.rs # TradingView UDF API + WebSocket + Canvas +│ │ ├── structs.rs # Data types (CandleData, Interval, WsMessage…) +│ │ ├── error.rs # Custom error types +│ │ └── lib.rs # Public module exports +│ ├── tests/ +│ │ ├── connection_test.rs # Database connection tests +│ │ ├── database_test.rs # CRUD & query tests +│ │ ├── scheduler_test.rs # Scheduler command & lifecycle tests +│ │ ├── sync_test.rs # Single symbol sync tests +│ │ └── sync_full_history_test.rs +│ ├── examples/ +│ │ ├── sync_all.rs # Sync all symbols (standard) +│ │ ├── sync_all_fast.rs # Sync all symbols (parallel with proxy pool) +│ │ └── sql.txt # Reference SQL for TimescaleDB setup +│ ├── Dockerfile +│ └── docker-compose.yml +│ +├── frontend/ # Charting Library frontend +│ ├── index.html # Main app (widget + watchlist + layouts) +│ ├── login.html # Google OAuth login page +│ ├── auth.js # AuthGuard — session management +│ ├── auth-config.js # OAuth & API configuration +│ ├── charting_library/ # Charting Library assets +│ ├── datafeeds/ # UDF datafeed adapter +│ ├── Dockerfile +│ └── docker-compose.yml +│ +└── references/ # Git submodules + ├── binance-rust/ # Binance connector SDK + └── library/ # Charting Library source +``` + +## 🛠️ Tech Stack + +| Layer | Technology | +|-------|-----------| +| **Backend** | Rust, Axum, sqlx, tokio, tokio-tungstenite | +| **Database** | PostgreSQL + TimescaleDB | +| **Frontend** | Charting Library v29.4, Vanilla JS | +| **Auth** | Google Identity Services (OAuth) | +| **Deployment** | Docker, Nginx, Docker Compose | + +## 🚀 Deployment + +For first-time users of Nginx Proxy Manager, TimescaleDB, PgAdmin, Docker, or Cloudflare — the easiest way to deploy is to let **Claude Opus 4.6** study this project and guide you through each step. If you encounter persistent issues, please open an Issue on this repository. + +### Option 1: Full Self-Hosted Deployment + +Deploy the entire stack on your own server with Docker and Nginx Proxy Manager as the reverse proxy. + +> [!NOTE] +> If you are unsure about any of the steps below, ask **Claude Opus 4.6** for guidance — it can walk you through each step in detail. + +#### Network Architecture + +``` +┌─────────────────── Docker Network: cycle ───────────────────┐ +│ │ +│ ┌──────────────────┐ ┌──────────────┐ │ +│ │ Nginx Proxy Mgr │ │ TimescaleDB │ │ +│ │ :80 / :443 │ │ :5432 │ │ +│ └────────┬─────────┘ └──────┬───────┘ │ +│ │ │ │ +│ │ │ │ +│ ┌────────▼──┐ ┌───────────┐ │ │ +│ │ Frontend │ │ Backend │───┘ │ +│ │ :80 │ │ :3000 │ │ +│ │ (nginx) │ │ (axum) │ │ +│ └───────────┘ └───────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ + ▲ + Public domain + view.yourdomain.com +``` + +All containers **must** run on the same Docker network `cycle` so they can communicate by container name. Only the **frontend** needs to be exposed to the public via Nginx Proxy Manager — the backend (port `3000`) is accessed internally by the frontend container through Docker DNS. + +--- + +#### Step 1: Create Docker Network + +```bash +docker network create cycle +``` + +--- + +#### Step 2: Database Setup (TimescaleDB) + +Create a `docker-compose.yml` for TimescaleDB on the `cycle` network: + +```yaml +# database/docker-compose.yml +services: + timescaledb: + image: timescale/timescaledb:latest-pg16 + container_name: timescaledb + networks: + - cycle + environment: + - POSTGRES_USER=quant + - POSTGRES_PASSWORD=your_secure_password + - POSTGRES_DB=crypto_database + volumes: + - timescaledb_data:/var/lib/postgresql/data + restart: unless-stopped + +volumes: + timescaledb_data: + +networks: + cycle: + external: true +``` + +```bash +docker compose up -d +``` + +Then initialize the schema using the SQL in [backend/examples/sql.txt](backend/examples/sql.txt): + +```bash +docker exec -i timescaledb psql -U quant -d crypto_database < backend/examples/sql.txt +``` + +> [!TIP] +> For database management, consider deploying **PgAdmin** alongside TimescaleDB. See [this guide](https://n8n.cathiefish.art/n8n-instances-twitter-ai-%E5%88%86%E6%9E%90%E7%9B%91%E6%8E%A7-5b706bb444d7) for PgAdmin setup instructions. For beginners, ask **Claude Opus 4.6** to walk you through it. + +#### Step 3: Deploy Backend + +**1. Configure `.env`** + +```bash +cd backend +cp .env.example .env +``` + +Edit `backend/.env`: + +```env +RUST_LOG="INFO,binance_sdk::common::utils=off,binance_sdk::common::websocket=off" + +DATABASE_URL="postgres://quant:your_secure_password@timescaledb:5432/crypto_database" + +TRACKED_SYMBOL=[BTCUSDT,ETHUSDT,BNBUSDT,SOLUSDT,XRPUSDT] + +# Proxy settings (optional — leave empty for direct connection) +PROXY_HOST=dc.your-proxy-provider.com +PROXY_USERNAME=your_username +PROXY_PASSWORD=your_password +PROXY_PROTOCOL=https/http/socks5 +PROXY_PORT_START=10000 +PROXY_PORT_END=10099 +``` + +> [!NOTE] +> `DATABASE_URL` uses the container name `timescaledb` as the hostname — this works because both containers are on the `cycle` network. You can also use the database container's absolute IP (find it with `docker inspect timescaledb --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'`). Make sure the database container is also on the `cycle` network. `TRACKED_SYMBOL` currently only supports **USDS-M Futures (Swap)**, not Spot. + +> [!WARNING] +> **Proxy is strongly recommended.** Historical data sync downloads from [Binance Data Archive](https://data.binance.vision/) for all tracked symbols. With a multi-port proxy pool (e.g. 100 concurrent connections), a full sync completes in hours. **Without a proxy, syncing may take several days.** If `PROXY_HOST` is left empty, the backend falls back to a single direct connection. + +**2. Build & Run** + +```bash +docker build -t backend . +docker compose up -d +``` + +--- + +#### Step 4: Deploy Frontend + +Before building the frontend container, you need to configure authentication and API connection. + +**1. Configure Google OAuth — `auth-config.js`** + +Since the frontend runs inside the Docker network, it accesses the backend via the **container name**: + +```js +// Use the backend container name as the hostname (internal Docker DNS) +window.API_CONFIG = { baseUrl: 'http://backend:3000' }; + +const AUTH_CONFIG = { + // Replace with your Google OAuth Client ID from GCP Console + clientId: 'YOUR_CLIENT_ID.apps.googleusercontent.com', + onSuccess: (user) => { console.log('Auth successful:', user.email); }, + onError: (error) => { console.error('Auth error:', error); } +}; +window.AUTH_CONFIG = AUTH_CONFIG; +``` + +To find the backend container IP (useful for debugging): + +```bash +$ docker inspect backend --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +172.18.0.3 +``` + +**2. Configure Email Whitelist — `login.html`** + +Add your Google account email to the whitelist: + +```js +const allowedEmails = ['your-email@gmail.com']; +``` + +> [!IMPORTANT] +> Google OAuth requires a Client ID from GCP Console. Tutorial placeholder — to be added. + +**3. Build & Run** + +```bash +cd frontend +docker build -t frontend . +docker compose up -d +``` + +> [!WARNING] +> The frontend and backend containers **must** be on the same Docker network (`cycle`). This is already configured in both `docker-compose.yml` files via `networks: cycle: external: true`. + +--- + +#### Step 5: Nginx Proxy Manager + +Nginx Proxy Manager must also run on the `cycle` network. Create a `docker-compose.yml`: + +```yaml +# npm/docker-compose.yml +services: + nginx-proxy-manager: + image: jc21/nginx-proxy-manager:latest + container_name: nginx-proxy-manager + networks: + - cycle + ports: + - "80:80" + - "443:443" + - "81:81" + volumes: + - npm_data:/data + - npm_letsencrypt:/etc/letsencrypt + restart: unless-stopped + +volumes: + npm_data: + npm_letsencrypt: + +networks: + cycle: + external: true +``` + +```bash +docker compose up -d +``` + +> [!TIP] +> For detailed Nginx Proxy Manager setup and Docker deployment best practices, see [this guide](https://n8n.cathiefish.art/rsshub-install-f5ad036a4dd9). For beginners, ask **Claude Opus 4.6** to walk you through it. + +Access the admin panel at `http://your-server-ip:81` (default: `admin@example.com` / `changeme`). + +**Create a Proxy Host for the frontend:** + +| Domain | Forward Hostname | Forward Port | SSL | +|--------|-----------------|--------------|-----| +| `view.yourdomain.com` | `frontend_tv` | `80` | ✅ Let's Encrypt | + +> [!TIP] +> The "Forward Hostname" uses the **container name** (not IP), which works because all containers share the `cycle` network. The backend does **not** need a public proxy — the frontend accesses it internally via Docker DNS (`http://backend:3000`). + +After setup, your platform will be accessible at `https://view.yourdomain.com`. + +--- + +### Option 2: Backend Self-Hosted + Frontend on Cloudflare Pages + +Build on top of **Option 1** — keep the same server-side setup (Docker network, TimescaleDB, backend), but additionally expose the backend API to the public internet and deploy the frontend to Cloudflare Pages instead of self-hosting it. + +> [!NOTE] +> If you are unsure about any of these steps, ask **Claude Opus 4.6** — this is a straightforward process. + +> [!CAUTION] +> **Do NOT** include "tradingview" in your Cloudflare Pages project name or custom domain. TradingView actively enforces their trademark and your deployment **will be taken down**. Use a neutral name like `view`, `chart`, or `crypto-dash`. + +#### Step 1: Backend + Database + +Complete **Option 1 Steps 1–3** (create `cycle` network, deploy TimescaleDB, deploy backend). + +#### Step 2: Expose Backend API via Nginx Proxy Manager + +Since the frontend will be served from Cloudflare (outside your Docker network), the backend API must be publicly accessible. Add a **second Proxy Host** in Nginx Proxy Manager: + +| Domain | Forward Hostname | Forward Port | SSL | +|--------|-----------------|--------------|-----| +| `api.yourdomain.com` | `backend` | `3000` | ✅ Let's Encrypt | + +> [!IMPORTANT] +> Enable **Websockets Support** for this proxy host — required for real-time candle streaming. + +#### Step 3: Configure Frontend + +Before deploying to Cloudflare, update the config files: + +**`auth-config.js`** — point `baseUrl` to the **public backend domain** from Step 2: + +```js +window.API_CONFIG = { baseUrl: 'https://api.yourdomain.com' }; + +const AUTH_CONFIG = { + clientId: 'YOUR_CLIENT_ID.apps.googleusercontent.com', + // ... +}; +``` + +**`login.html`** — add your email to the whitelist: + +```js +const allowedEmails = ['your-email@gmail.com']; +``` + +#### Step 4: Deploy to Cloudflare Pages + +1. Fork or clone this repository to your own GitHub account +2. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → **Workers & Pages** → **Create** +3. Select **Pages** → **Connect to Git** +4. Authorize Cloudflare to access your GitHub account and select the repository +5. Configure the build settings: + +| Setting | Value | +|---------|-------| +| Production branch | `main` | +| Build command | *(leave empty)* | +| Build output directory | `frontend` | + +6. Click **Save and Deploy** + +Cloudflare will assign a `*.pages.dev` domain. You can add a custom domain in **Pages** → **Custom domains**. + +--- + +## 📜 License + +MIT + + +Generated By Claude Opus 4.6 diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..d5bacee --- /dev/null +++ b/README_CN.md @@ -0,0 +1,400 @@ +# 📊 View — 加密货币图表平台 + +自建的实时加密货币数据采集与可视化平台,适用于 **Binance USDS-M 永续合约**。结合高性能 Rust 后端与专业图表前端,提供机构级市场数据工具。 + +这个项目主要是我自用的,实在没太多精力维护。我也不擅长前端,前端代码可能不太规范。欢迎大家发起 Pull Request 和 Issue! + +如您所见,这并不是一个简单的一键启动的傻瓜工程,也不是面向小白的玩具项目。请原谅我没有时间也没有能力将其简单化。最简单的部署方式就是让 **Claude Opus 4.6** 研究完整个项目,然后让它来指导你启动。 + +如果后端部署实在太过困难,您可以暂时直接使用我的公开后端 API 端口:`api-view.cathiefish.org`,这样您只需将前端部署到 Cloudflare Pages 即可。注意该端口仅保证提供 **BTCUSDT, ETHUSDT, SOLUSDT, BNBUSDT, XRPUSDT, SUIUSDT** 的数据。请参阅下方[部署](#-部署)章节了解如何配置。 + +![tv-1](https://img.cathiefish.art/ns/tv-1.png) +![tv-2](https://img.cathiefish.art/ns/tv-2.png) +![tv-3](https://img.cathiefish.art/ns/tv-3.png) + +## ✨ 功能 + +### 数据采集与存储 +- [x] **实时 WebSocket 推流** — 从 Binance 合并流实时接收 1 分钟 K 线数据,支持自动重连和缺口回补 +- [x] **三级历史数据同步** — 月度 ZIP → 每日 ZIP → REST API,从 [Binance 数据归档](https://data.binance.vision/) 尽可能快速回补 +- [x] **TimescaleDB 时序存储** — Hypertable 优化,支持 `time_bucket` 聚合,覆盖 8 个时间周期(1m, 5m, 15m, 1h, 4h, 1D, 1W, 1M) +- [x] **代理池支持** — 最多 100 个并发代理客户端,用于高吞吐量并行下载 +- [ ] **Binance 现货支持** — 现货市场数据采集,使用 `.P` 后缀区分合约与现货 +- [ ] **MCP 支持** — Model Context Protocol 集成 + +### 图表与可视化 +- [x] **专业图表引擎** — 完整的指标和画图工具支持 +- [x] **自定义指标** — 净成交量(NV-C)和累积成交量差(CVD-C) +- [x] **多图表布局** — 单图、垂直分割、水平分割、1 左 + 2 右布局,分隔线可拖动 +- [x] **画布持久化** — 按品种保存命名画布,每 5 分钟自动保存 +- [x] **深色/浅色主题** — 主题切换同步到图表组件和 UI + +### 自选列表与交互 +- [x] **实时自选列表侧栏** — 实时价格、24 小时涨跌幅、拖拽排序 +- [x] **多自定义列表** — 创建和管理多个自选列表 +- [ ] **前端品种管理** — 从前端 UI 添加/移除追踪品种(目前 `TRACKED_SYMBOL` 仅通过后端 `.env` 文件控制) + +### 安全与部署 +- [x] **Google OAuth 保护** — 邮箱白名单访问控制,144 小时会话持久化 +- [x] **Docker 就绪** — 多阶段构建,Docker Compose 共享网络 +- [ ] **Google Firebase 集成** — 通过 Firebase 连接后端 API,实现托管认证与托管部署 + +## 📁 项目结构 + +``` +view/ +├── backend/ # Rust 数据引擎 & API 服务 +│ ├── src/ +│ │ ├── main.rs # Axum HTTP 服务启动 +│ │ ├── binance_collector.rs # WebSocket 实时采集 + REST 同步 +│ │ ├── historical_downloader.rs # Binance 数据归档 (ZIP) 下载器 +│ │ ├── database.rs # TimescaleDB 操作与聚合 +│ │ ├── scheduler.rs # 任务调度与采集器生命周期 +│ │ ├── klinechart.rs # KlineChart REST API 处理 +│ │ ├── tradingview.rs # TradingView UDF API + WebSocket + Canvas +│ │ ├── structs.rs # 数据类型(CandleData, Interval, WsMessage…) +│ │ ├── error.rs # 自定义错误类型 +│ │ └── lib.rs # 公开模块导出 +│ ├── tests/ +│ │ ├── connection_test.rs # 数据库连接测试 +│ │ ├── database_test.rs # CRUD 与查询测试 +│ │ ├── scheduler_test.rs # 调度器命令与生命周期测试 +│ │ ├── sync_test.rs # 单品种同步测试 +│ │ └── sync_full_history_test.rs +│ ├── examples/ +│ │ ├── sync_all.rs # 同步所有品种(标准模式) +│ │ ├── sync_all_fast.rs # 同步所有品种(代理池并行) +│ │ └── sql.txt # TimescaleDB 建表 SQL +│ ├── Dockerfile +│ └── docker-compose.yml +│ +├── frontend/ # 图表库前端 +│ ├── index.html # 主应用(图表 + 自选列表 + 布局) +│ ├── login.html # Google OAuth 登录页 +│ ├── auth.js # AuthGuard — 会话管理 +│ ├── auth-config.js # OAuth 与 API 配置 +│ ├── charting_library/ # 图表库资源 +│ ├── datafeeds/ # UDF 数据源适配器 +│ ├── Dockerfile +│ └── docker-compose.yml +│ +└── references/ # Git 子模块 + ├── binance-rust/ # Binance 连接器 SDK + └── library/ # 图表库源码 +``` + +## 🛠️ 技术栈 + +| 层级 | 技术 | +|------|------| +| **后端** | Rust, Axum, sqlx, tokio, tokio-tungstenite | +| **数据库** | PostgreSQL + TimescaleDB | +| **前端** | Charting Library v29.4, Vanilla JS | +| **认证** | Google Identity Services (OAuth) | +| **部署** | Docker, Nginx, Docker Compose | + +## 🚀 部署 + +对于第一次接触 Nginx Proxy Manager、TimescaleDB、PgAdmin、Docker 或 Cloudflare 的用户,最简单的部署方式就是让 **Claude Opus 4.6** 研究这个项目,然后让它一步步指导你部署。如果持续失败,请在本仓库提交 Issue。 + +### 方案一:完全自建部署 + +在自己的服务器上部署全套服务,使用 Docker 和 Nginx Proxy Manager 作为反向代理。 + +> [!NOTE] +> 如果不清楚以下任何步骤,请咨询 **Claude Opus 4.6**,它可以详细指导你完成每一步。 + +#### 网络架构 + +``` +┌─────────────────── Docker 网络: cycle ──────────────────────┐ +│ │ +│ ┌──────────────────┐ ┌──────────────┐ │ +│ │ Nginx Proxy Mgr │ │ TimescaleDB │ │ +│ │ :80 / :443 │ │ :5432 │ │ +│ └────────┬─────────┘ └──────┬───────┘ │ +│ │ │ │ +│ │ │ │ +│ ┌────────▼──┐ ┌───────────┐ │ │ +│ │ Frontend │ │ Backend │───┘ │ +│ │ :80 │ │ :3000 │ │ +│ │ (nginx) │ │ (axum) │ │ +│ └───────────┘ └───────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ + ▲ + 公网域名 + view.yourdomain.com +``` + +所有容器**必须**在同一个 Docker 网络 `cycle` 中运行,这样才能通过容器名互相通信。只有**前端**需要通过 Nginx Proxy Manager 暴露到公网 —— 后端(端口 `3000`)由前端容器通过 Docker 内部 DNS 访问。 + +--- + +#### 第一步:创建 Docker 网络 + +```bash +docker network create cycle +``` + +--- + +#### 第二步:数据库部署(TimescaleDB) + +为 TimescaleDB 创建 `docker-compose.yml`,加入 `cycle` 网络: + +```yaml +# database/docker-compose.yml +services: + timescaledb: + image: timescale/timescaledb:latest-pg16 + container_name: timescaledb + networks: + - cycle + environment: + - POSTGRES_USER=quant + - POSTGRES_PASSWORD=your_secure_password + - POSTGRES_DB=crypto_database + volumes: + - timescaledb_data:/var/lib/postgresql/data + restart: unless-stopped + +volumes: + timescaledb_data: + +networks: + cycle: + external: true +``` + +```bash +docker compose up -d +``` + +然后使用 [backend/examples/sql.txt](backend/examples/sql.txt) 中的 SQL 初始化数据库: + +```bash +docker exec -i timescaledb psql -U quant -d crypto_database < backend/examples/sql.txt +``` + +> [!TIP] +> 建议同时部署 **PgAdmin** 用于数据库管理。详见[这篇教程](https://n8n.cathiefish.art/n8n-instances-twitter-ai-%E5%88%86%E6%9E%90%E7%9B%91%E6%8E%A7-5b706bb444d7)。新手建议直接让 **Claude Opus 4.6** 来指导。 + +#### 第三步:部署后端 + +**1. 配置 `.env`** + +```bash +cd backend +cp .env.example .env +``` + +编辑 `backend/.env`: + +```env +RUST_LOG="INFO,binance_sdk::common::utils=off,binance_sdk::common::websocket=off" + +DATABASE_URL="postgres://quant:your_secure_password@timescaledb:5432/crypto_database" + +TRACKED_SYMBOL=[BTCUSDT,ETHUSDT,BNBUSDT,SOLUSDT,XRPUSDT] + +# 代理设置(可选 — 留空则直连) +PROXY_HOST=dc.your-proxy-provider.com +PROXY_USERNAME=your_username +PROXY_PASSWORD=your_password +PROXY_PROTOCOL=https +PROXY_PORT_START=10000 +PROXY_PORT_END=10099 +``` + +> [!NOTE] +> `DATABASE_URL` 使用容器名 `timescaledb` 作为主机名 —— 因为两个容器都在 `cycle` 网络中。也可以使用数据库容器的绝对 IP(通过 `docker inspect timescaledb --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'` 查询)。确保数据库容器也在 `cycle` 网络中。`TRACKED_SYMBOL` 目前仅支持 **USDS-M 永续合约(Swap)**,不支持现货。 + +> [!WARNING] +> **强烈建议配置代理。** 后端需要从 [Binance 数据归档](https://data.binance.vision/) 下载所有追踪品种的历史数据。使用多端口代理池(如 100 个并发连接)。**不使用代理的话,同步可能需要几天时间。** 如果 `PROXY_HOST` 留空,后端将使用单一直连。 + +**2. 构建并运行** + +```bash +docker build -t backend . +docker compose up -d +``` + +--- + +#### 第四步:部署前端 + +构建前端容器之前,需要先配置认证和 API 连接。 + +**1. 配置 Google OAuth — `auth-config.js`** + +前端在 Docker 网络内部运行,通过**容器名**访问后端: + +```js +// 使用后端容器名作为主机名(Docker 内部 DNS) +window.API_CONFIG = { baseUrl: 'http://backend:3000' }; + +const AUTH_CONFIG = { + // 替换为你从 GCP Console 获取的 Google OAuth Client ID + clientId: 'YOUR_CLIENT_ID.apps.googleusercontent.com', + onSuccess: (user) => { console.log('Auth successful:', user.email); }, + onError: (error) => { console.error('Auth error:', error); } +}; +window.AUTH_CONFIG = AUTH_CONFIG; +``` + +查询后端容器 IP(调试用): + +```bash +$ docker inspect backend --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +172.18.0.3 +``` + +**2. 配置邮箱白名单 — `login.html`** + +将你的 Google 邮箱添加到白名单: + +```js +const allowedEmails = ['your-email@gmail.com']; +``` + +> [!IMPORTANT] +> Google OAuth 需要从 GCP Console 获取 Client ID。教程待补充。 + +**3. 构建并运行** + +```bash +cd frontend +docker build -t frontend . +docker compose up -d +``` + +> [!WARNING] +> 前端和后端容器**必须**在同一个 Docker 网络(`cycle`)中。两个 `docker-compose.yml` 文件已通过 `networks: cycle: external: true` 配置好。 + +--- + +#### 第五步:Nginx Proxy Manager + +Nginx Proxy Manager 也必须在 `cycle` 网络中运行。创建 `docker-compose.yml`: + +```yaml +# npm/docker-compose.yml +services: + nginx-proxy-manager: + image: jc21/nginx-proxy-manager:latest + container_name: nginx-proxy-manager + networks: + - cycle + ports: + - "80:80" + - "443:443" + - "81:81" + volumes: + - npm_data:/data + - npm_letsencrypt:/etc/letsencrypt + restart: unless-stopped + +volumes: + npm_data: + npm_letsencrypt: + +networks: + cycle: + external: true +``` + +```bash +docker compose up -d +``` + +> [!TIP] +> Nginx Proxy Manager 的详细部署和 Docker 最佳实践,请参阅[这篇教程](https://n8n.cathiefish.art/rsshub-install-f5ad036a4dd9)。新手建议直接让 **Claude Opus 4.6** 来指导。 + +访问管理面板 `http://你的服务器IP:81`(默认账号:`admin@example.com` / `changeme`)。 + +**为前端创建 Proxy Host:** + +| 域名 | 转发主机名 | 转发端口 | SSL | +|------|-----------|---------|-----| +| `view.yourdomain.com` | `frontend_tv` | `80` | ✅ Let's Encrypt | + +> [!TIP] +> "转发主机名"使用的是**容器名**(不是 IP),因为所有容器共享 `cycle` 网络。后端**不需要**公网代理 —— 前端通过 Docker 内部 DNS(`http://backend:3000`)访问。 + +部署完成后,即可通过 `https://view.yourdomain.com` 访问。 + +--- + +### 方案二:后端自建 + 前端部署到 Cloudflare Pages + +在**方案一**的基础上,额外将后端 API 暴露到公网,并将前端部署到 Cloudflare Pages,而不是自建托管。 + +> [!NOTE] +> 如果不清楚以下步骤,请咨询 **Claude Opus 4.6** —— 这是很简单的操作。 + +> [!CAUTION] +> Cloudflare Pages 项目名和自定义域名中**不要**包含 "tradingview"。TradingView 会主动维权,你的部署**会被下架**。使用中性名称如 `view`、`chart`、`crypto-dash`。 + +#### 第一步:后端 + 数据库 + +完成**方案一的第一至三步**(创建 `cycle` 网络、部署 TimescaleDB、部署后端)。 + +#### 第二步:通过 Nginx Proxy Manager 暴露后端 API + +由于前端将从 Cloudflare 提供服务(在 Docker 网络外部),后端 API 必须可以从公网访问。在 Nginx Proxy Manager 中添加一个 **Proxy Host**: + +| 域名 | 转发主机名 | 转发端口 | SSL | +|------|-----------|---------|-----| +| `api.yourdomain.com` | `backend` | `3000` | ✅ Let's Encrypt | + +> [!IMPORTANT] +> 为此 Proxy Host 启用 **Websockets Support** —— 实时 K 线推送需要 WebSocket。 + +#### 第三步:配置前端 + +部署到 Cloudflare 之前,更新配置文件: + +**`auth-config.js`** —— 将 `baseUrl` 指向第二步的**公网后端域名**: + +```js +window.API_CONFIG = { baseUrl: 'https://api.yourdomain.com' }; + +const AUTH_CONFIG = { + clientId: 'YOUR_CLIENT_ID.apps.googleusercontent.com', + // ... +}; +``` + +**`login.html`** —— 添加你的邮箱到白名单: + +```js +const allowedEmails = ['your-email@gmail.com']; +``` + +#### 第四步:部署到 Cloudflare Pages + +1. Fork 或 Clone 本仓库到你自己的 GitHub 账号 +2. 进入 [Cloudflare 控制台](https://dash.cloudflare.com/) → **Workers & Pages** → **创建** +3. 选择 **Pages** → **连接到 Git** +4. 授权 Cloudflare 访问你的 GitHub 账号,选择仓库 +5. 配置构建设置: + +| 设置 | 值 | +|------|---| +| 生产分支 | `main` | +| 构建命令 | *(留空)* | +| 构建输出目录 | `frontend` | + +6. 点击 **保存并部署** + +Cloudflare 会分配一个 `*.pages.dev` 域名。你可以在 **Pages** → **自定义域** 中添加自定义域名。 + +--- + +## 📜 许可证 + +MIT + + +Generated By Claude Opus 4.6 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..1762ab5 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,14 @@ +RUST_LOG="INFO,binance_sdk::common::utils=off,binance_sdk::common::websocket=off" + +DATABASE_URL="postgres://xxx:xxxxxxx" + +TRACKED_SYMBOL=[BTCUSDT,XRPUSDT,BNBUSDT,SOLUSDT,ETHUSDT] + +# Proxy settings (optional — leave empty or remove for direct connection) +# Required for fast multi-proxy parallel downloads from Binance Data Archive +PROXY_HOST= +PROXY_USERNAME= +PROXY_PASSWORD= +PROXY_PROTOCOL=https +PROXY_PORT_START=10000 +PROXY_PORT_END=10099 \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..54466f5 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +/target + diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 0000000..0822090 --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,3981 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +dependencies = [ + "axum-core", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite 0.28.0", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "axum", + "binance-sdk", + "chrono", + "csv", + "dotenv", + "env_logger", + "futures", + "json", + "log", + "rand 0.9.2", + "reqwest", + "serde", + "serde_json", + "sqlx", + "thiserror 2.0.17", + "tokio", + "tokio-tungstenite 0.26.2", + "tower-http", + "zip", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "binance-sdk" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b21e91b13015a41f753231d9e8c8bfde04f1135f4541509173229fcd8b68c6" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "derive_builder", + "ed25519-dalek", + "flate2", + "futures", + "hex", + "hmac", + "http", + "once_cell", + "openssl", + "rand 0.8.5", + "regex", + "reqwest", + "reqwest-middleware", + "rust_decimal", + "rustc_version", + "serde", + "serde_json", + "serde_repr", + "sha2", + "thiserror 2.0.17", + "tokio", + "tokio-native-tls", + "tokio-stream", + "tokio-tungstenite 0.26.2", + "tokio-util", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "reqwest-middleware" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562ceb5a604d3f7c885a792d42c199fd8af239d0a51b2fa6a78aafa092452b04" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "rust_decimal_macros", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8c0cb48f413ebe24dc2d148788e0efbe09ba3e011d9277162f2eaf8e1069a3" +dependencies = [ + "quote", + "syn 2.0.111", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.111", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.111", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite 0.26.2", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.28.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.17", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..9267100 --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +[dependencies] + +binance-sdk = { version = "35.0.0", features = ["derivatives_trading_usds_futures", "spot"] } +rand = "0.9" +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] } + +axum = { version = "0.8", features = ["ws"] } +tower-http = { version = "0.6", features = ["cors"] } + +dotenv = "0.15.0" +json = "0.12.4" +log = "0.4.29" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +thiserror = "2.0.17" +tokio = {version = "1.48.0", features = ["full"] } +env_logger = "0.11" +reqwest = { version = "0.12", features = ["stream"] } +zip = "2.2" +chrono = "0.4" +csv = "1.3" +futures = "0.3" +tokio-tungstenite = { version = "0.26", features = ["native-tls"] } \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..706c9a0 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +# Build stage +FROM rust:1.89-slim-bookworm AS builder +WORKDIR /app +RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* +COPY Cargo.toml Cargo.lock ./ +COPY src ./src +RUN cargo build --release + +# Runtime stage +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/release/backend /usr/local/bin/backend +EXPOSE 3000 +CMD ["backend"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..f2b99fb --- /dev/null +++ b/backend/README.md @@ -0,0 +1,296 @@ +# 📊 Binance USDS Futures — Data Collection & API Backend + +A high-performance Rust backend that collects, stores, and serves Binance USDS-M Futures K-line data in real-time. Designed as the data engine for custom charting frontends — supports both [KlineChart](https://klinecharts.com/) and [TradingView](https://www.tradingview.com/charting-library-docs/) out of the box. + +## ✨ Features + +- **Real-time WebSocket streaming** — Subscribe to 1-minute K-line updates via Binance combined streams, with auto-reconnect and backfill on disconnect +- **Multi-strategy historical sync** — Monthly ZIP → Daily ZIP → REST API fallback for fastest possible backfill +- **TimescaleDB-powered storage** — Hypertable-optimized with `time_bucket` aggregation for 8 timeframes (1m, 5m, 15m, 1h, 4h, 1D, 1W, 1M) +- **Dual API interface** — KlineChart REST API + TradingView UDF-compatible datafeed +- **Live WebSocket broadcast** — Push real-time candle updates to connected frontend clients +- **Canvas persistence** — Save/load chart drawings per symbol to local filesystem +- **Net Volume & Taker Buy Volume** — Custom indicators included in every response +- **Proxy pool support** — Up to 100 proxy clients (port 10000–10099) for high-throughput parallel downloads +- **Docker ready** — Multi-stage build with minimal runtime image + +## 🏗️ Architecture + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Axum HTTP Server (:3000) │ +│ │ +│ ┌─────────────────────┐ ┌──────────────────────────┐ │ +│ │ KlineChart API │ │ TradingView UDF API │ │ +│ │ /api/klines │ │ /config /history /ws │ │ +│ │ /api/symbols │ │ /symbols /search │ │ +│ │ /api/status │ │ /canvas/* │ │ +│ └────────┬────────────┘ └────────────┬─────────────┘ │ +│ │ │ │ +│ └──────────┬──────────────────────┘ │ +│ ▼ │ +│ ┌────────────────┐ │ +│ │ Scheduler │ │ +│ │ (Command Bus) │ │ +│ └───┬────────┬───┘ │ +│ │ │ │ +│ ┌──────────▼──┐ ┌──▼──────────────────────┐ │ +│ │ Binance │ │ Historical Downloader │ │ +│ │ Collector │ │ (ZIP + REST backfill) │ │ +│ │ (WebSocket)│ └─────────────────────────┘ │ +│ └─────────────┘ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ DatabaseHandler │ │ +│ │ (TimescaleDB + batch) │ │ +│ └─────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## 📁 Project Structure + +``` +src/ +├── main.rs # Axum server bootstrap & route composition +├── binance_collector.rs # WebSocket real-time collection + REST sync +├── historical_downloader.rs # Binance data archive (ZIP) downloader +├── database.rs # TimescaleDB operations, batch insert, aggregation +├── scheduler.rs # Task coordination & collector lifecycle +├── klinechart.rs # KlineChart REST API handlers +├── tradingview.rs # TradingView UDF API + WebSocket + Canvas +├── structs.rs # Data types (CandleData, Interval, WsMessage…) +├── error.rs # Custom error types (CollectorError, SchedulerError) +└── lib.rs # Public module exports + +tests/ +├── connection_test.rs # Database connection tests +├── database_test.rs # CRUD & query tests +├── scheduler_test.rs # Scheduler command & lifecycle tests +├── sync_test.rs # Single symbol sync tests +└── sync_full_history_test.rs # Full historical backfill tests + +examples/ +├── sync_all.rs # Sync all symbols (standard) +├── sync_all_fast.rs # Sync all symbols (parallel with proxy pool) +└── sql.txt # Reference SQL for TimescaleDB setup +``` + +## 🚀 Quick Start + +### Prerequisites + +- **Rust** 1.70+ (edition 2021) +- **PostgreSQL** with [TimescaleDB](https://docs.timescale.com/) extension +- (Recommended) Third-party rotating proxy with multi-port support for parallel downloads + +### Environment Setup + +```bash +cp .env.example .env +# Edit .env with your values +``` + +`.env.example`: +```env +RUST_LOG="INFO,binance_sdk::common::utils=off,binance_sdk::common::websocket=off" +DATABASE_URL="postgres://user:password@host:5432/crypto_database" +TRACKED_SYMBOL=[BTCUSDT,XRPUSDT,BNBUSDT,SOLUSDT,ETHUSDT] + +# Proxy settings (optional — leave empty for direct connection) +PROXY_HOST=dc.your-proxy-provider.com +PROXY_USERNAME=your_username +PROXY_PASSWORD=your_password +PROXY_PROTOCOL=https +PROXY_PORT_START=10000 +PROXY_PORT_END=10099 +``` + +> [!WARNING] +> **Proxy is strongly recommended.** The backend downloads historical data from [Binance Data Archive](https://data.binance.vision/) for all tracked symbols. With a multi-port proxy pool (e.g. 100 concurrent connections), a full sync completes in minutes. **Without a proxy, syncing may take several days** due to single-connection rate limits. If `PROXY_HOST` is left empty, the backend falls back to a single direct connection. + +### Run + +```bash +# Development +cargo run + +# Release build +cargo build --release +./target/release/backend + +# Run tests +cargo test +``` + +### Docker + +```bash +# Build image +docker build -t backend . + +# Using docker-compose (connects to existing `cycle` network) +docker compose up -d +``` + +## 📡 API Reference + +### KlineChart API + +| Method | Endpoint | Description | +|--------|----------|-------------| +| **GET** | `/api/klines/{symbol}` | Query K-line data | +| **GET** | `/api/symbols` | List all tracked symbols | +| **POST** | `/api/symbols` | Add symbol to tracking (triggers backfill) | +| **DELETE** | `/api/symbols/{symbol}` | Remove symbol from tracking | +| **GET** | `/api/status` | Get scheduler status | + +**Query Parameters** for `/api/klines/{symbol}`: + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `limit` | `i64` | `800` | Number of candles to return | +| `interval` | `string` | `1m` | Timeframe: `1m`, `5m`, `15m`, `1h`, `4h`, `1d`, `1w`, `1M` | +| `end_time` | `i64` | *now* | Unix timestamp (ms) upper bound | + +
+📄 Response Example + +```json +{ + "success": true, + "data": [ + { + "symbol": "BTCUSDT", + "timestamp": 1704067200000, + "open": 42000.0, + "high": 42100.0, + "low": 41900.0, + "close": 42050.0, + "volume": 1000.5, + "taker_buy_volume": 600.3, + "net_volume": 200.1, + "is_closed": true + } + ] +} +``` + +
+ +--- + +### TradingView UDF API + +Fully compatible with the [TradingView UDF Datafeed API](https://www.tradingview.com/charting-library-docs/latest/connecting_data/UDF/). + +| Method | Endpoint | Description | +|--------|----------|-------------| +| **GET** | `/config` | Datafeed configuration | +| **GET** | `/time` | Server time (seconds) | +| **GET** | `/symbols` | Resolve symbol info | +| **GET** | `/search` | Search symbols | +| **GET** | `/history` | Historical OHLCV data (includes `nv` and `tbv`) | +| **GET** | `/tracked-symbols` | List configured symbols | +| **GET** | `/daily-opens` | Daily open prices for all symbols | +| **WS** | `/ws` | Real-time K-line push via WebSocket | + +
+📄 History Response Example + +```json +{ + "s": "ok", + "t": [1704067200, 1704153600], + "o": [42000.0, 42050.0], + "h": [42100.0, 42200.0], + "l": [41900.0, 41950.0], + "c": [42050.0, 42150.0], + "v": [1000.5, 1200.3], + "nv": [200.1, -150.5], + "tbv": [600.3, 525.4] +} +``` + +
+ +
+📄 WebSocket Protocol + +**Subscribe:** +```json +{ "type": "subscribe", "data": { "symbols": ["BTCUSDT", "ETHUSDT"] } } +``` + +**Kline Update (server → client):** +```json +{ "type": "kline", "data": { "symbol": "BTCUSDT", "timestamp": 1704067200000, "open": 42000.0, "high": 42100.0, "low": 41900.0, "close": 42050.0, "volume": 1000.5, "taker_buy_volume": 600.3, "net_volume": 200.1, "is_closed": false } } +``` + +**Keepalive:** `ping` / `pong` + +
+ +--- + +### Canvas API (Drawing Persistence) + +Save and load chart drawings per symbol to the local filesystem. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| **GET** | `/canvas/list` | List saved canvases for a symbol | +| **GET** | `/canvas/load` | Load canvas drawings | +| **POST** | `/canvas/save` | Save canvas drawings | +| **DELETE** | `/canvas/delete` | Delete a canvas | + +## ⚙️ Core Components + +### BinanceCollector +- Connects to Binance WebSocket combined streams for real-time 1m K-line data +- Supports up to **50 symbols per connection** (Binance limit); auto-splits into multiple connections +- **Auto-reconnect** with gap detection — backfills missed data on disconnect +- REST API sync with rate limiting (150ms interval, ~480 req/min) + +### HistoricalDownloader +- **3-tier download strategy**: Monthly ZIP → Daily ZIP → REST API (fastest to slowest) +- Downloads from [Binance Data Archive](https://data.binance.vision/) for bulk historical data +- Concurrent downloads across proxy pool for maximum throughput +- CSV parsing from ZIP archives + +### DatabaseHandler +- **TimescaleDB** hypertable for time-series optimization +- **Batch insert**: 100 candles or 5-second flush timeout +- `time_bucket` aggregation for multi-timeframe queries (1m → 1M) +- Gap detection and data integrity checks +- Data cutoff: only syncs data from **2024-01-01 UTC** onwards + +### Scheduler +- Command-based control via `mpsc` channels: + - `AddSymbol` — backfill + restart collector + - `RemoveSymbol` — deactivate + restart collector + - `RestartCollector` / `GetStatus` / `Shutdown` +- Manages full lifecycle: symbol tracking → historical backfill → real-time streaming + +## 📦 Dependencies + +| Crate | Purpose | +|-------|---------| +| `axum` | Web framework with WebSocket support | +| `sqlx` | Async PostgreSQL / TimescaleDB driver | +| `tokio` | Async runtime | +| `binance-sdk` | Official Binance connector (USDS futures + spot) | +| `tokio-tungstenite` | WebSocket client for Binance streams | +| `reqwest` | HTTP client for REST API & archive downloads | +| `tower-http` | CORS middleware | +| `serde` / `serde_json` | Serialization | +| `chrono` | Date/time handling | +| `csv` / `zip` | Historical data archive parsing | +| `thiserror` | Custom error types | +| `env_logger` | Logging | + +## 📜 License + +MIT + + +Generated By Claude Opus 4.6 \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..3cd64bc --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,17 @@ +services: + backend: + image: ghcr.io/exchanges-lab/statistic/backend:v1.2 + container_name: backend + networks: + - cycle + #ports: + # - "3000:3000" + env_file: + - .env + volumes: + - ./storage:/storage + restart: unless-stopped + +networks: + cycle: + external: true diff --git a/backend/examples/sql.txt b/backend/examples/sql.txt new file mode 100644 index 0000000..0d1514a --- /dev/null +++ b/backend/examples/sql.txt @@ -0,0 +1,47 @@ +-- Create klines table for 1-minute candlestick data +CREATE TABLE IF NOT EXISTS klines_1m ( + symbol VARCHAR(20) NOT NULL, + timestamp BIGINT NOT NULL, + open DOUBLE PRECISION NOT NULL, + high DOUBLE PRECISION NOT NULL, + low DOUBLE PRECISION NOT NULL, + close DOUBLE PRECISION NOT NULL, + volume DOUBLE PRECISION NOT NULL, + taker_buy_volume DOUBLE PRECISION NOT NULL, + net_volume DOUBLE PRECISION NOT NULL, + PRIMARY KEY (symbol, timestamp) +); + +-- Convert to TimescaleDB hypertable for better time-series performance +-- chunk_time_interval = 7 days (in milliseconds: 7 * 24 * 60 * 60 * 1000 = 604800000) +SELECT create_hypertable('klines_1m', 'timestamp', + chunk_time_interval => 604800000, + if_not_exists => TRUE +); + +-- Create index for faster queries by symbol +CREATE INDEX IF NOT EXISTS idx_klines_1m_symbol ON klines_1m (symbol, timestamp DESC); + +-- Table for tracking active symbols +CREATE TABLE IF NOT EXISTS tracked_symbols ( + symbol VARCHAR(20) PRIMARY KEY, + added_at TIMESTAMP DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE +); + +-- Insert some default symbols +INSERT INTO tracked_symbols (symbol) VALUES + ('btcusdt'), + ('ethusdt') +ON CONFLICT (symbol) DO NOTHING; + +-- Useful queries: + +-- Get latest timestamp for a symbol +-- SELECT MAX(timestamp) FROM klines_1m WHERE symbol = 'BTCUSDT'; + +-- Get latest N candles for a symbol +-- SELECT * FROM klines_1m WHERE symbol = 'BTCUSDT' ORDER BY timestamp DESC LIMIT 100; + +-- Get all active symbols +-- SELECT symbol FROM tracked_symbols WHERE is_active = TRUE; diff --git a/backend/examples/sync_all.rs b/backend/examples/sync_all.rs new file mode 100644 index 0000000..34a231a --- /dev/null +++ b/backend/examples/sync_all.rs @@ -0,0 +1,137 @@ +use backend::{BinanceCollector, CandleData, DatabaseHandler}; + +use log::{info, error}; +use dotenv::dotenv; +use tokio::sync::mpsc; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +#[tokio::main] +async fn main() { + dotenv().ok(); + env_logger::init(); + + // 1. Connect to database + let database_url = std::env::var("DATABASE_URL") + .expect("DATABASE_URL must be set"); + + info!("Connecting to database..."); + let db = Arc::new( + DatabaseHandler::new(&database_url) + .await + .expect("Failed to connect to database") + ); + + // 2. Get all symbols (filter USDT pairs only) + info!("Fetching all symbols from Binance..."); + let all_symbols = BinanceCollector::get_symbol().await.unwrap(); + let symbols: Vec<_> = all_symbols + .into_iter() + .filter(|s| s.symbol.ends_with("USDT")) + .collect(); + info!("Got {} USDT symbols to sync", symbols.len()); + + // 3. Build clients + info!("Building clients..."); + let clients = BinanceCollector::build_clients().await; + info!("Built {} working clients", clients.len()); + + if clients.is_empty() { + error!("No working clients, exiting"); + return; + } + + // 4. Sync each symbol one by one + let total_symbols = symbols.len(); + + for (i, symbol) in symbols.into_iter().enumerate() { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + let start_time = symbol.start_timestamp; + let total_range_ms = now - start_time; + // Estimate total candles (1 candle per minute) + let estimated_total = (total_range_ms / 60000) as u64; + + info!("[{}/{}] Starting sync for {} (estimated {} candles)", + i + 1, total_symbols, symbol.symbol, estimated_total); + + // Create channels + let (tx, rx) = mpsc::channel::(100000); + let (db_tx, db_rx) = mpsc::channel::(100000); + + // Progress tracking + let candle_count = Arc::new(AtomicU64::new(0)); + let last_progress = Arc::new(AtomicU64::new(0)); + + // Progress monitor and forward task + let symbol_name = symbol.symbol.clone(); + let candle_count_clone = candle_count.clone(); + let last_progress_clone = last_progress.clone(); + let idx = i + 1; + + let progress_handle = tokio::spawn(async move { + let mut rx = rx; + while let Some(candle) = rx.recv().await { + let count = candle_count_clone.fetch_add(1, Ordering::Relaxed) + 1; + + // Calculate progress based on received candles vs estimated total + let progress = if estimated_total > 0 { + ((count as f64 / estimated_total as f64) * 100.0).min(100.0) as u64 + } else { + 100 + }; + + // Report every 20% + let last = last_progress_clone.load(Ordering::Relaxed); + let milestone = (progress / 20) * 20; + if milestone > last && milestone <= 100 { + if last_progress_clone.compare_exchange( + last, milestone, Ordering::Relaxed, Ordering::Relaxed + ).is_ok() { + info!("[{}/{}] {} progress: {}% ({}/{} candles)", + idx, total_symbols, symbol_name, milestone, count, estimated_total); + } + } + + // Forward to database + let _ = db_tx.send(candle).await; + } + }); + + // Database consumer + let db_clone = db.clone(); + let consumer_handle = tokio::spawn(async move { + db_clone.start_consumer(db_rx).await; + }); + + // Sync this symbol + let result = BinanceCollector::sync_from_scratch( + symbol.symbol.clone(), + symbol.start_timestamp, + clients.clone(), + tx, + ).await; + + // Wait for tasks to finish + let _ = progress_handle.await; + let _ = consumer_handle.await; + + let final_count = candle_count.load(Ordering::Relaxed); + + match result { + Ok(count) => { + info!("[{}/{}] {} completed: {} candles", + i + 1, total_symbols, symbol.symbol, count); + } + Err(e) => { + error!("[{}/{}] {} failed: {} (received: {} candles)", + i + 1, total_symbols, symbol.symbol, e, final_count); + } + } + } + + info!("All symbols synced!"); +} diff --git a/backend/examples/sync_all_fast.rs b/backend/examples/sync_all_fast.rs new file mode 100644 index 0000000..b91507f --- /dev/null +++ b/backend/examples/sync_all_fast.rs @@ -0,0 +1,162 @@ +use backend::{BinanceCollector, CandleData, DatabaseHandler, HistoricalDownloader}; + +use log::{info, error}; +use dotenv::dotenv; +use tokio::sync::mpsc; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +#[tokio::main] +async fn main() { + dotenv().ok(); + env_logger::init(); + + // 1. Connect to database + let database_url = std::env::var("DATABASE_URL") + .expect("DATABASE_URL must be set"); + + info!("Connecting to database..."); + let db = Arc::new( + DatabaseHandler::new(&database_url) + .await + .expect("Failed to connect to database") + ); + + // 2. Get all USDT symbols + info!("Fetching all symbols from Binance..."); + let all_symbols = BinanceCollector::get_symbol().await.unwrap(); + let symbols: Vec<_> = all_symbols + .into_iter() + .filter(|s| s.symbol.ends_with("USDT")) + .collect(); + info!("Got {} USDT symbols to sync", symbols.len()); + + // 3. Build clients + info!("Building download clients (with proxies)..."); + let download_clients = HistoricalDownloader::build_clients().await; + + info!("Building API clients (for recent data)..."); + let api_clients = BinanceCollector::build_clients().await; + info!("Built {} download clients, {} API clients", download_clients.len(), api_clients.len()); + + // Get last complete month timestamp + let archive_end = HistoricalDownloader::last_complete_month_end(); + info!("Archive data available until: {}", archive_end); + + // Use the global cutoff timestamp + info!("Data cutoff: 2024-01-01 00:00:00 UTC (timestamp: {})", backend::DATA_CUTOFF_TIMESTAMP); + + // 4. Sync each symbol + let total_symbols = symbols.len(); + + for (i, symbol) in symbols.into_iter().enumerate() { + info!("========================================"); + info!("[{}/{}] {} - Starting fast sync", i + 1, total_symbols, symbol.symbol); + + // Check if we have data already + let latest_ts = db.get_latest_timestamp(&symbol.symbol).await.ok().flatten(); + + let start_time = match latest_ts { + Some(ts) => { + info!("[{}/{}] {} - Has data until {}, continuing from there", + i + 1, total_symbols, symbol.symbol, ts); + ts + 60000 + } + None => { + // No existing data, use the later of symbol start or cutoff + let actual_start = symbol.start_timestamp.max(backend::DATA_CUTOFF_TIMESTAMP); + if symbol.start_timestamp < backend::DATA_CUTOFF_TIMESTAMP { + info!("[{}/{}] {} - Original start is before 2024-01-01, starting from cutoff instead", + i + 1, total_symbols, symbol.symbol); + } + actual_start + } + }; + + // Create channels + let (tx, rx) = mpsc::channel::(500000); + let (db_tx, db_rx) = mpsc::channel::(500000); + + // Progress tracking + let candle_count = Arc::new(AtomicU64::new(0)); + let candle_count_clone = candle_count.clone(); + let symbol_name = symbol.symbol.clone(); + let idx = i + 1; + + // Progress forwarder + let progress_handle = tokio::spawn(async move { + let mut rx = rx; + let mut last_report = 0u64; + + while let Some(candle) = rx.recv().await { + let count = candle_count_clone.fetch_add(1, Ordering::Relaxed) + 1; + + if count - last_report >= 500000 { + info!("[{}/{}] {} - Progress: {} candles", idx, total_symbols, symbol_name, count); + last_report = count; + } + + let _ = db_tx.send(candle).await; + } + }); + + // DB consumer (batch insert) + let db_clone = db.clone(); + let consumer_handle = tokio::spawn(async move { + db_clone.start_consumer(db_rx).await; + }); + + // STEP 1: Download historical data from archive (with proxies) + if start_time < archive_end && !download_clients.is_empty() { + info!("[{}/{}] {} - Downloading from archive with {} proxies...", + i + 1, total_symbols, symbol.symbol, download_clients.len()); + + match HistoricalDownloader::download_symbol_with_clients( + &symbol.symbol, + start_time, + &download_clients, + tx.clone(), + ).await { + Ok(count) => info!("[{}/{}] {} - Archive download: {} candles", + i + 1, total_symbols, symbol.symbol, count), + Err(e) => error!("[{}/{}] {} - Archive download failed: {}", + i + 1, total_symbols, symbol.symbol, e), + } + } + + // STEP 2: Sync recent data via API + let api_start = archive_end.max(start_time); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + if now - api_start > 60000 && !api_clients.is_empty() { + info!("[{}/{}] {} - Syncing recent data via API...", i + 1, total_symbols, symbol.symbol); + + match BinanceCollector::sync_from_scratch( + symbol.symbol.clone(), + api_start, + api_clients.clone(), + tx.clone(), + ).await { + Ok(count) => info!("[{}/{}] {} - API sync: {} candles", + i + 1, total_symbols, symbol.symbol, count), + Err(e) => error!("[{}/{}] {} - API sync failed: {}", + i + 1, total_symbols, symbol.symbol, e), + } + } + + // Close channel and wait + drop(tx); + let _ = progress_handle.await; + let _ = consumer_handle.await; + + let final_count = candle_count.load(Ordering::Relaxed); + info!("[{}/{}] {} - Complete: {} total candles", + i + 1, total_symbols, symbol.symbol, final_count); + } + + info!("========================================"); + info!("All symbols synced!"); +} diff --git a/backend/src/binance_collector.rs b/backend/src/binance_collector.rs new file mode 100644 index 0000000..d876287 --- /dev/null +++ b/backend/src/binance_collector.rs @@ -0,0 +1,709 @@ +use crate::error::*; +use crate::structs::*; +use crate::historical_downloader::HistoricalDownloader; + +use binance_sdk::{ + config::{ConfigurationRestApi, ProxyConfig, ProxyAuth}, + derivatives_trading_usds_futures::{ + DerivativesTradingUsdsFuturesRestApi, + rest_api::{ + RestApi, + KlineCandlestickDataIntervalEnum, KlineCandlestickDataParams, KlineCandlestickDataResponseItemInner, + }, + websocket_streams::KlineCandlestickStreamsResponseK, + }, +}; + +use futures::StreamExt; +use log::{debug, error, info, warn}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::{mpsc, RwLock}; +use reqwest::Client; +use tokio_tungstenite::{connect_async, tungstenite::Message}; +use rand::Rng; +use serde::Deserialize; + +/// Read proxy configuration from environment variables. +/// Returns None if PROXY_HOST is not set (direct connection). +fn proxy_config_from_env(port: u16) -> Option { + let host = std::env::var("PROXY_HOST").ok()?; + if host.is_empty() { + return None; + } + let username = std::env::var("PROXY_USERNAME").unwrap_or_default(); + let password = std::env::var("PROXY_PASSWORD").unwrap_or_default(); + let protocol = std::env::var("PROXY_PROTOCOL").unwrap_or_else(|_| "https".to_string()); + + let auth = if username.is_empty() { + None + } else { + Some(ProxyAuth { + username, + password, + }) + }; + + Some(ProxyConfig { + host, + port, + protocol: Some(protocol), + auth, + }) +} + +// Combined stream response wrapper +#[derive(Debug, Deserialize)] +struct CombinedStreamWrapper { + #[allow(dead_code)] + stream: String, + data: CombinedStreamData, +} + +#[derive(Debug, Deserialize)] +struct CombinedStreamData { + s: Option, + k: Option, +} + +const BATCH_SIZE: i64 = 1000; +const ONE_MINUTE_MS: i64 = 60_000; +const MAX_SYMBOLS_PER_WS: usize = 50; // Binance limit per WebSocket connection +// Rate limit: 2400 weight/min, limit=1000 costs 5 weight +// 2400/5 = 480 requests/min = 8 req/sec = 125ms interval +// Use 150ms for safety margin +const REQUEST_INTERVAL_MS: u64 = 150; + +pub struct BinanceCollector { + pub symbols: Vec, + rest_client: RestApi, + last_closed_timestamps: Arc>>, +} + +impl BinanceCollector { + pub fn new(symbols: Vec) -> Self { + assert!(!symbols.is_empty(), "symbols cannot be empty"); + + let symbols: Vec = symbols.into_iter().map(|s| s.to_lowercase()).collect(); + + let port: u16 = rand::rng().random_range(10036..=10066); + + let mut config_builder = ConfigurationRestApi::builder() + .timeout(10000); + + if let Some(proxy) = proxy_config_from_env(port) { + config_builder = config_builder.proxy(proxy); + } + + let rest_client_config = config_builder + .build() + .expect("Failed to initialize the rest api client"); + + let rest_client = DerivativesTradingUsdsFuturesRestApi::production(rest_client_config); + + Self { + symbols, + rest_client, + last_closed_timestamps: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Backfill K-line data for a specific symbol from start_time to end_time. + /// Uses limit=1000 per request (weight=5, ~480 requests/min allowed). + /// Returns total number of candles fetched. + pub async fn backfill( + &self, + symbol: &str, + start_time: i64, + end_time: i64, + candle_tx: &mpsc::Sender, + ) -> Result { + let symbol_upper = symbol.to_uppercase(); + let symbol_lower = symbol.to_lowercase(); + let mut current_start = start_time; + let mut total_count: u64 = 0; + + info!( + "Starting backfill for {} from {} to {}", + symbol_upper, start_time, end_time + ); + + while current_start < end_time { + let params = KlineCandlestickDataParams::builder( + symbol_upper.clone(), + KlineCandlestickDataIntervalEnum::Interval1m, + ) + .start_time(Some(current_start)) + .end_time(Some(end_time)) + .limit(Some(BATCH_SIZE)) + .build() + .map_err(|e| CollectorError::RestApiError(e.to_string()))?; + + let response = self.rest_client + .kline_candlestick_data(params) + .await + .map_err(|e| CollectorError::RestApiError(e.to_string()))?; + + let klines = response.data().await + .map_err(|e| CollectorError::RestApiError(e.to_string()))?; + + let batch_count = klines.len(); + if batch_count == 0 { + break; + } + + let mut last_timestamp = current_start; + + for kline in &klines { + if let Some(candle) = Self::parse_rest_kline(&symbol_upper, kline) { + last_timestamp = candle.timestamp; + + if let Err(e) = candle_tx.send(candle).await { + warn!("Failed to send candle: {}", e); + } + + total_count += 1; + } + } + + // Update last_closed_timestamp for this symbol + { + let mut timestamps = self.last_closed_timestamps.write().await; + timestamps.insert(symbol_lower.clone(), last_timestamp); + } + + debug!( + "Fetched {} klines for {}, total: {}, last_ts: {}", + batch_count, symbol_upper, total_count, last_timestamp + ); + + current_start = last_timestamp + ONE_MINUTE_MS; + + if batch_count < BATCH_SIZE as usize { + break; + } + + tokio::time::sleep(tokio::time::Duration::from_millis(REQUEST_INTERVAL_MS)).await; + } + + info!( + "Backfill completed for {}: {} candles", + symbol_upper, total_count + ); + + Ok(total_count) + } + + /// Backfill all symbols in the collector. + pub async fn backfill_all( + &self, + start_time: i64, + end_time: i64, + candle_tx: &mpsc::Sender, + ) -> Result { + let mut total_count: u64 = 0; + + for symbol in &self.symbols.clone() { + let count = self.backfill(symbol, start_time, end_time, candle_tx).await?; + total_count += count; + } + + Ok(total_count) + } + + /// Start WebSocket streams for real-time K-line data on all symbols. + /// Creates multiple connections if symbols > MAX_SYMBOLS_PER_WS. + /// Automatically reconnects and backfills missed data on disconnect. + pub async fn start_stream( + &self, + candle_tx: mpsc::Sender, + ) -> Result<(), CollectorError> { + let total_symbols = self.symbols.len(); + let num_connections = (total_symbols + MAX_SYMBOLS_PER_WS - 1) / MAX_SYMBOLS_PER_WS; + + info!( + "Starting {} WebSocket connections for {} symbols", + num_connections, total_symbols + ); + + // Split symbols into batches + let batches: Vec> = self.symbols + .chunks(MAX_SYMBOLS_PER_WS) + .map(|chunk| chunk.to_vec()) + .collect(); + + loop { + // Start all WebSocket connections in parallel + let mut handles = Vec::new(); + + for (i, batch) in batches.iter().enumerate() { + let batch_symbols = batch.clone(); + let tx = candle_tx.clone(); + let last_ts = self.last_closed_timestamps.clone(); + + let handle = tokio::spawn(async move { + Self::run_single_ws_connection(i, batch_symbols, tx, last_ts).await + }); + handles.push(handle); + } + + // Wait for any connection to fail + let mut all_ok = true; + for (i, handle) in handles.into_iter().enumerate() { + match handle.await { + Ok(Ok(())) => info!("WS connection {} ended normally", i), + Ok(Err(e)) => { + error!("WS connection {} error: {}", i, e); + all_ok = false; + } + Err(e) => { + error!("WS connection {} join error: {}", i, e); + all_ok = false; + } + } + } + + if all_ok { + break; + } + + // Reconnect after error + error!("WebSocket error. Reconnecting in 5 seconds..."); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + // Backfill missed data + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + let timestamps = self.last_closed_timestamps.read().await.clone(); + + for symbol in &self.symbols { + if let Some(&last_ts) = timestamps.get(symbol) { + if now - last_ts > ONE_MINUTE_MS { + info!("Backfilling missed data for {} from {}", symbol, last_ts); + if let Err(e) = self.backfill(symbol, last_ts, now, &candle_tx).await { + warn!("Backfill failed for {}: {}", symbol, e); + } + } + } + } + } + Ok(()) + } + + /// Run a single WebSocket connection for a batch of symbols (combined streams) + async fn run_single_ws_connection( + connection_id: usize, + symbols: Vec, + candle_tx: mpsc::Sender, + last_closed_timestamps: Arc>>, + ) -> Result<(), CollectorError> { + // Build combined stream URL: wss://fstream.binance.com/stream?streams=symbol1@kline_1m/symbol2@kline_1m + let streams: Vec = symbols.iter() + .map(|s| format!("{}@kline_1m", s.to_lowercase())) + .collect(); + let url = format!("wss://fstream.binance.com/stream?streams={}", streams.join("/")); + + let (ws_stream, _) = connect_async(&url) + .await + .map_err(|e| CollectorError::ConnectionFailed(e.to_string()))?; + + info!("WS {} connected with {} symbols (combined stream)", connection_id, symbols.len()); + + let (_, mut read) = ws_stream.split(); + + // Process incoming messages + while let Some(msg_result) = read.next().await { + match msg_result { + Ok(Message::Text(text)) => { + // Combined stream format: {"stream":"btcusdt@kline_1m","data":{...}} + if let Ok(wrapper) = serde_json::from_str::(&text) { + if let Some(k) = wrapper.data.k { + let symbol = wrapper.data.s.unwrap_or_default(); + let candle = Self::parse_ws_kline(&symbol, &k); + + if let Err(e) = candle_tx.send(candle.clone()).await { + warn!("Failed to send candle: {}", e); + } + + if candle.is_closed { + let mut timestamps = last_closed_timestamps.write().await; + timestamps.insert(candle.symbol.to_lowercase(), candle.timestamp); + + debug!( + "Kline closed: {} ts={} c={:.2} net_vol={:.4}", + candle.symbol, candle.timestamp, candle.close, candle.net_volume + ); + } + } + } + } + Ok(Message::Ping(data)) => { + debug!("WS {} received ping", connection_id); + // tungstenite auto-responds to pings + let _ = data; + } + Ok(Message::Close(_)) => { + info!("WS {} received close", connection_id); + break; + } + Ok(_) => {} + Err(e) => { + error!("WS {} error: {}", connection_id, e); + break; + } + } + } + + Err(CollectorError::ConnectionFailed("Disconnected".to_string())) + } + + fn parse_rest_kline(symbol: &str, kline: &Vec) -> Option { + if kline.len() < 12 { + return None; + } + + let timestamp = match &kline[0] { + KlineCandlestickDataResponseItemInner::Integer(v) => *v, + _ => return None, + }; + + let open = Self::parse_string_field(&kline[1])?; + let high = Self::parse_string_field(&kline[2])?; + let low = Self::parse_string_field(&kline[3])?; + let close = Self::parse_string_field(&kline[4])?; + let volume = Self::parse_string_field(&kline[5])?; + let taker_buy_volume = Self::parse_string_field(&kline[9])?; + + let net_volume = CandleData::calculate_net_volume(volume, taker_buy_volume); + + Some(CandleData { + symbol: symbol.to_uppercase(), + timestamp, + open, + high, + low, + close, + volume, + taker_buy_volume, + net_volume, + is_closed: true, + }) + } + + fn parse_string_field(item: &KlineCandlestickDataResponseItemInner) -> Option { + match item { + KlineCandlestickDataResponseItemInner::String(s) => s.parse().ok(), + _ => None, + } + } + + fn parse_ws_kline(symbol: &str, k: &KlineCandlestickStreamsResponseK) -> CandleData { + let timestamp = k.t.unwrap_or(0); + let open = k.o.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0.0); + let high = k.h.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0.0); + let low = k.l.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0.0); + let close = k.c.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0.0); + let volume = k.v.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0.0); + let taker_buy_volume = k.v_uppercase.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0.0); + let is_closed = k.x.unwrap_or(false); + + let net_volume = CandleData::calculate_net_volume(volume, taker_buy_volume); + + CandleData { + symbol: symbol.to_uppercase(), + timestamp, + open, + high, + low, + close, + volume, + taker_buy_volume, + net_volume, + is_closed, + } + } + + + /// Get all tradable symbol in current market + pub async fn get_symbol() -> Result, CollectorError> { + + let port: u16 = rand::rng().random_range(10036..=10066); + + let mut config_builder = ConfigurationRestApi::builder() + .timeout(10000); + + if let Some(proxy) = proxy_config_from_env(port) { + config_builder = config_builder.proxy(proxy); + } + + let rest_client_config = config_builder + .build() + .expect("Failed to initialize the rest api client"); + + let rest_client = DerivativesTradingUsdsFuturesRestApi::production(rest_client_config); + + let mut symbol_list = Vec::new(); + + let response = rest_client + .exchange_information() + .await + .map_err(|e| CollectorError::GetSymbolError(e.to_string()))?; + + let data = response.data().await.unwrap(); + + let symbol_vec = data.symbols.unwrap(); + + for item in symbol_vec { + + if item.contract_type.unwrap() == "PERPETUAL" { + + let symbol = Symbol { + + symbol: item.symbol.unwrap(), + start_timestamp: item.onboard_date.unwrap(), + }; + + symbol_list.push(symbol); + } + } + + Ok(symbol_list) + } + + + /// Build proxy list (port 10000-10099) and create clients + pub async fn build_clients() -> Vec> { + let mut handles = Vec::new(); + + let port_start: u16 = std::env::var("PROXY_PORT_START") + .ok().and_then(|v| v.parse().ok()).unwrap_or(10000); + let port_end: u16 = std::env::var("PROXY_PORT_END") + .ok().and_then(|v| v.parse().ok()).unwrap_or(10099); + + for port in port_start..=port_end { + let handle = tokio::spawn(async move { + let mut config_builder = ConfigurationRestApi::builder() + .timeout(5000); + + if let Some(proxy) = proxy_config_from_env(port) { + config_builder = config_builder.proxy(proxy); + } + + let config = config_builder + .build() + .ok()?; + + let client = DerivativesTradingUsdsFuturesRestApi::production(config); + + // Test connection + if client.check_server_time().await.ok()?.data().await.is_ok() { + Some(Arc::new(client)) + } else { + None + } + }); + handles.push(handle); + } + + let mut clients = Vec::new(); + for handle in handles { + if let Ok(Some(client)) = handle.await { + clients.push(client); + } + } + + info!("Built {} working clients", clients.len()); + clients + } + + /// Comprehensive sync: Monthly ZIP → Daily ZIP → API (fastest to slowest) + pub async fn sync_comprehensive( + symbol: String, + start_time: i64, + api_clients: Vec>, + http_clients: &[Arc], + candle_tx: mpsc::Sender, + ) -> Result { + let mut total = 0u64; + let mut current_start = start_time; + + // Phase 1: Monthly ZIP download (complete months only) + let month_end = HistoricalDownloader::last_complete_month_end(); + if current_start < month_end && !http_clients.is_empty() { + info!("{}: Phase 1 - Monthly ZIP download", symbol); + match HistoricalDownloader::download_symbol_with_clients( + &symbol, + current_start, + http_clients, + candle_tx.clone(), + ).await { + Ok(count) => { + total += count; + if count > 0 { + current_start = month_end; + info!("{}: Monthly ZIP done, {} candles", symbol, count); + } + } + Err(e) => warn!("{}: Monthly ZIP failed: {}", symbol, e), + } + } + + // Phase 2: Daily ZIP download (current month's completed days) + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let today_start = (now / (24 * 60 * 60 * 1000)) * (24 * 60 * 60 * 1000); + + if current_start < today_start && !http_clients.is_empty() { + info!("{}: Phase 2 - Daily ZIP download", symbol); + match HistoricalDownloader::download_days_with_clients( + &symbol, + current_start, + http_clients, + candle_tx.clone(), + ).await { + Ok(count) => { + total += count; + if count > 0 { + current_start = today_start; + info!("{}: Daily ZIP done, {} candles", symbol, count); + } + } + Err(e) => warn!("{}: Daily ZIP failed: {}", symbol, e), + } + } + + // Phase 3: API for remaining (today's data) + if current_start < now && !api_clients.is_empty() { + info!("{}: Phase 3 - API sync for today", symbol); + match Self::sync_from_scratch( + symbol.clone(), + current_start, + api_clients, + candle_tx, + ).await { + Ok(count) => { + total += count; + info!("{}: API sync done, {} candles", symbol, count); + } + Err(e) => warn!("{}: API sync failed: {}", symbol, e), + } + } + + info!("{}: Comprehensive sync complete, total {} candles", symbol, total); + Ok(total) + } + + /// Sync single symbol using ALL clients in parallel (each client handles a time segment) + pub async fn sync_from_scratch( + symbol: String, + start_time: i64, + clients: Vec>, + candle_tx: mpsc::Sender, + ) -> Result { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + let num_clients = clients.len(); + if num_clients == 0 { + return Err(CollectorError::RestApiError("No clients".to_string())); + } + + let total_duration = now - start_time; + let segment_size = total_duration / num_clients as i64; + + info!( + "Syncing {} with {} clients, {} -> {} ({} ms per segment)", + symbol, num_clients, start_time, now, segment_size + ); + + // Each client gets a time segment + let mut handles = Vec::new(); + + for (i, client) in clients.into_iter().enumerate() { + let seg_start = start_time + (i as i64 * segment_size); + let seg_end = if i == num_clients - 1 { + now // Last segment goes to now + } else { + start_time + ((i + 1) as i64 * segment_size) + }; + + let sym = symbol.clone(); + let tx = candle_tx.clone(); + + handles.push(tokio::spawn(async move { + Self::sync_segment(sym, seg_start, seg_end, client, tx).await + })); + } + + // Wait and sum results + let mut total = 0u64; + for (i, h) in handles.into_iter().enumerate() { + match h.await { + Ok(Ok(count)) => { + total += count; + debug!("Client {} done: {} candles", i, count); + } + Ok(Err(e)) => error!("Client {} error: {}", i, e), + Err(e) => error!("Client {} join error: {}", i, e), + } + } + + info!("{} sync complete: {} candles", symbol, total); + Ok(total) + } + + /// Sync a specific time segment + async fn sync_segment( + symbol: String, + start_time: i64, + end_time: i64, + client: Arc, + candle_tx: mpsc::Sender, + ) -> Result { + let mut current = start_time; + let mut count = 0u64; + + while current < end_time { + let params = KlineCandlestickDataParams::builder( + symbol.clone(), + KlineCandlestickDataIntervalEnum::Interval1m, + ) + .start_time(Some(current)) + .end_time(Some(end_time)) + .limit(Some(BATCH_SIZE)) + .build() + .map_err(|e| CollectorError::RestApiError(e.to_string()))?; + + let res = client + .kline_candlestick_data(params) + .await + .map_err(|e| CollectorError::RestApiError(e.to_string()))?; + + let klines = res.data().await + .map_err(|e| CollectorError::RestApiError(e.to_string()))?; + + if klines.is_empty() { break; } + + let batch_len = klines.len(); + for kline in &klines { + if let Some(candle) = Self::parse_rest_kline(&symbol, kline) { + current = candle.timestamp + ONE_MINUTE_MS; + let _ = candle_tx.send(candle).await; + count += 1; + } + } + + if batch_len < BATCH_SIZE as usize { break; } + tokio::time::sleep(tokio::time::Duration::from_millis(REQUEST_INTERVAL_MS)).await; + } + + Ok(count) + } +} diff --git a/backend/src/database.rs b/backend/src/database.rs new file mode 100644 index 0000000..ea6f021 --- /dev/null +++ b/backend/src/database.rs @@ -0,0 +1,479 @@ +use crate::structs::*; + +use log::{debug, error, info}; +use sqlx::{PgPool, postgres::PgPoolOptions}; +use tokio::sync::mpsc; +use tokio::time::{Duration, interval}; + +const BUFFER_SIZE: usize = 100; +const FLUSH_INTERVAL_SECS: u64 = 5; + +pub struct DatabaseHandler { + pool: PgPool, +} + +impl DatabaseHandler { + pub async fn new(database_url: &str) -> Result { + let pool = PgPoolOptions::new() + .max_connections(20) + .min_connections(5) + .connect(database_url) + .await?; + + info!("Database connected (pool: 5-20 connections)"); + + Ok(Self { pool }) + } + + /// Start consuming from rx and batch insert into database + pub async fn start_consumer(&self, mut rx: mpsc::Receiver) { + let mut buffer: Vec = Vec::with_capacity(BUFFER_SIZE); + let mut flush_timer = interval(Duration::from_secs(FLUSH_INTERVAL_SECS)); + + info!("Database consumer started"); + + loop { + tokio::select! { + // Receive candle data + candle = rx.recv() => { + match candle { + Some(c) => { + buffer.push(c); + if buffer.len() >= BUFFER_SIZE { + self.flush_buffer(&mut buffer).await; + } + } + None => { + // Channel closed, flush remaining and exit + if !buffer.is_empty() { + self.flush_buffer(&mut buffer).await; + } + info!("Database consumer stopped (channel closed)"); + break; + } + } + } + // Flush on timer + _ = flush_timer.tick() => { + if !buffer.is_empty() { + self.flush_buffer(&mut buffer).await; + } + } + } + } + } + + async fn flush_buffer(&self, buffer: &mut Vec) { + if buffer.is_empty() { + return; + } + + let count = buffer.len(); + + match self.batch_insert(buffer).await { + Ok(_) => { + debug!("Inserted {} candles", count); + } + Err(e) => { + error!("Failed to insert {} candles: {}", count, e); + } + } + + buffer.clear(); + } + + async fn batch_insert(&self, candles: &[CandleData]) -> Result<(), sqlx::Error> { + if candles.is_empty() { + return Ok(()); + } + + // Deduplicate by (symbol, timestamp) - keep last occurrence + use std::collections::HashMap; + let mut dedup_map: HashMap<(String, i64), &CandleData> = HashMap::new(); + for c in candles { + dedup_map.insert((c.symbol.clone(), c.timestamp), c); + } + let unique_candles: Vec<&CandleData> = dedup_map.into_values().collect(); + + if unique_candles.is_empty() { + return Ok(()); + } + + // Build batch insert query + let mut query = String::from( + "INSERT INTO klines_1m (symbol, timestamp, open, high, low, close, volume, taker_buy_volume, net_volume) VALUES " + ); + + let mut values: Vec = Vec::with_capacity(unique_candles.len()); + + for (i, _) in unique_candles.iter().enumerate() { + let idx = i * 9; + values.push(format!( + "(${}, ${}, ${}, ${}, ${}, ${}, ${}, ${}, ${})", + idx + 1, idx + 2, idx + 3, idx + 4, idx + 5, idx + 6, idx + 7, idx + 8, idx + 9 + )); + } + + query.push_str(&values.join(", ")); + query.push_str(" ON CONFLICT (symbol, timestamp) DO UPDATE SET open = EXCLUDED.open, high = EXCLUDED.high, low = EXCLUDED.low, close = EXCLUDED.close, volume = EXCLUDED.volume, taker_buy_volume = EXCLUDED.taker_buy_volume, net_volume = EXCLUDED.net_volume"); + + let mut query_builder = sqlx::query(&query); + + for c in unique_candles { + query_builder = query_builder + .bind(&c.symbol) + .bind(c.timestamp) + .bind(c.open) + .bind(c.high) + .bind(c.low) + .bind(c.close) + .bind(c.volume) + .bind(c.taker_buy_volume) + .bind(c.net_volume); + } + + query_builder.execute(&self.pool).await?; + + Ok(()) + } + + /// Get latest timestamp for a symbol + pub async fn get_latest_timestamp(&self, symbol: &str) -> Result, sqlx::Error> { + let row: Option<(Option,)> = sqlx::query_as( + "SELECT MAX(timestamp) FROM klines_1m WHERE symbol = $1" + ) + .bind(symbol.to_uppercase()) + .fetch_optional(&self.pool) + .await?; + + Ok(row.and_then(|r| r.0)) + } + + /// Get today's UTC 00:00 open price for all tracked symbols + pub async fn get_daily_opens(&self) -> Result, sqlx::Error> { + use chrono::Utc; + let now = Utc::now(); + let today_start = now.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp_millis(); + + let rows: Vec<(String, f64)> = sqlx::query_as( + "SELECT symbol, open FROM klines_1m WHERE timestamp = $1" + ) + .bind(today_start) + .fetch_all(&self.pool) + .await?; + + Ok(rows.into_iter().collect()) + } + + /// Get all active symbols from tracked_symbols table + pub async fn get_active_symbols(&self) -> Result, sqlx::Error> { + let rows: Vec<(String,)> = sqlx::query_as( + "SELECT symbol FROM tracked_symbols WHERE is_active = TRUE" + ) + .fetch_all(&self.pool) + .await?; + + Ok(rows.into_iter().map(|r| r.0).collect()) + } + + /// Get tracked symbols from TRACKED_SYMBOL env variable + /// Format: TRACKED_SYMBOL=[BTCUSDT,ETHUSDT,BNBUSDT] + pub fn get_symbols_from_env() -> Vec { + std::env::var("TRACKED_SYMBOL") + .unwrap_or_default() + .trim_start_matches('[') + .trim_end_matches(']') + .split(',') + .map(|s| s.trim().to_uppercase()) + .filter(|s| !s.is_empty()) + .collect() + } + + /// Insert candle directly (for single inserts) + pub async fn insert_candle(&self, candle: &CandleData) -> Result<(), sqlx::Error> { + sqlx::query( + "INSERT INTO klines_1m (symbol, timestamp, open, high, low, close, volume, taker_buy_volume, net_volume) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (symbol, timestamp) DO UPDATE SET + open = EXCLUDED.open, high = EXCLUDED.high, low = EXCLUDED.low, + close = EXCLUDED.close, volume = EXCLUDED.volume, + taker_buy_volume = EXCLUDED.taker_buy_volume, net_volume = EXCLUDED.net_volume" + ) + .bind(&candle.symbol) + .bind(candle.timestamp) + .bind(candle.open) + .bind(candle.high) + .bind(candle.low) + .bind(candle.close) + .bind(candle.volume) + .bind(candle.taker_buy_volume) + .bind(candle.net_volume) + .execute(&self.pool) + .await?; + + Ok(()) + } + + /// Add a new symbol to tracked_symbols + pub async fn add_symbol(&self, symbol: &str) -> Result<(), sqlx::Error> { + let symbol_lower = symbol.to_lowercase(); + + sqlx::query( + "INSERT INTO tracked_symbols (symbol, is_active) + VALUES ($1, TRUE) + ON CONFLICT (symbol) DO UPDATE SET is_active = TRUE" + ) + .bind(&symbol_lower) + .execute(&self.pool) + .await?; + + info!("Added symbol to tracking: {}", symbol_lower); + Ok(()) + } + + /// Remove (deactivate) a symbol from tracking + pub async fn remove_symbol(&self, symbol: &str) -> Result<(), sqlx::Error> { + let symbol_lower = symbol.to_lowercase(); + + sqlx::query( + "UPDATE tracked_symbols SET is_active = FALSE WHERE symbol = $1" + ) + .bind(&symbol_lower) + .execute(&self.pool) + .await?; + + info!("Removed symbol from tracking: {}", symbol_lower); + Ok(()) + } + + /// Check if a symbol is currently being tracked + pub async fn is_symbol_tracked(&self, symbol: &str) -> Result { + let symbol_lower = symbol.to_lowercase(); + + let row: Option<(bool,)> = sqlx::query_as( + "SELECT is_active FROM tracked_symbols WHERE symbol = $1" + ) + .bind(&symbol_lower) + .fetch_optional(&self.pool) + .await?; + + Ok(row.map(|r| r.0).unwrap_or(false)) + } + + /// Delete all kline data for a symbol (use with caution) + pub async fn delete_symbol_data(&self, symbol: &str) -> Result { + let symbol_upper = symbol.to_uppercase(); + + let result = sqlx::query( + "DELETE FROM klines_1m WHERE symbol = $1" + ) + .bind(&symbol_upper) + .execute(&self.pool) + .await?; + + let deleted = result.rows_affected(); + info!("Deleted {} candles for symbol: {}", deleted, symbol_upper); + Ok(deleted) + } + + /// Get klines for a symbol with limit and optional end_time + pub async fn get_klines(&self, symbol: &str, limit: i64, end_time: Option) -> Result, sqlx::Error> { + let symbol_upper = symbol.to_uppercase(); + + let rows: Vec<(String, i64, f64, f64, f64, f64, f64, f64, f64)> = if let Some(et) = end_time { + sqlx::query_as( + "SELECT symbol, timestamp, open, high, low, close, volume, taker_buy_volume, net_volume + FROM klines_1m + WHERE symbol = $1 AND timestamp <= $3 + ORDER BY timestamp DESC + LIMIT $2" + ) + .bind(&symbol_upper) + .bind(limit) + .bind(et) + .fetch_all(&self.pool) + .await? + } else { + sqlx::query_as( + "SELECT symbol, timestamp, open, high, low, close, volume, taker_buy_volume, net_volume + FROM klines_1m + WHERE symbol = $1 + ORDER BY timestamp DESC + LIMIT $2" + ) + .bind(&symbol_upper) + .bind(limit) + .fetch_all(&self.pool) + .await? + }; + + let mut candles: Vec = rows.into_iter().map(|r| CandleData { + symbol: r.0, + timestamp: r.1, + open: r.2, + high: r.3, + low: r.4, + close: r.5, + volume: r.6, + taker_buy_volume: r.7, + net_volume: r.8, + is_closed: true, + }).collect(); + + // Reverse to chronological order (oldest first) + candles.reverse(); + + Ok(candles) + } + + /// Get klines aggregated to a specific interval using TimescaleDB time_bucket + pub async fn get_klines_aggregated( + &self, + symbol: &str, + interval: crate::Interval, + limit: i64, + end_time: Option + ) -> Result, sqlx::Error> { + use crate::Interval; + + // For 1m interval, just return raw data + if interval == Interval::Min1 { + return self.get_klines(symbol, limit, end_time).await; + } + + let symbol_upper = symbol.to_uppercase(); + + // Calculate interval in milliseconds for time range filtering + let interval_ms: i64 = match interval { + Interval::Min1 => 60_000, + Interval::Min5 => 5 * 60_000, + Interval::Min15 => 15 * 60_000, + Interval::Hour1 => 60 * 60_000, + Interval::Hour4 => 4 * 60 * 60_000, + Interval::Day1 => 24 * 60 * 60_000, + Interval::Week1 => 7 * 24 * 60 * 60_000, + Interval::Month1 => 30 * 24 * 60 * 60_000, + }; + + // Convert interval to PostgreSQL interval string + let interval_str = match interval { + Interval::Min1 => "1 minute", + Interval::Min5 => "5 minutes", + Interval::Min15 => "15 minutes", + Interval::Hour1 => "1 hour", + Interval::Hour4 => "4 hours", + Interval::Day1 => "1 day", + Interval::Week1 => "1 week", + Interval::Month1 => "1 month", + }; + + // Calculate time range to limit scan (add 10% buffer) + let et = end_time.unwrap_or_else(|| chrono::Utc::now().timestamp_millis()); + let time_range_needed = interval_ms * limit * 11 / 10; + let start_time = et - time_range_needed; + + // Use TimescaleDB time_bucket for efficient aggregation with time range filter + let query = format!( + r#" + SELECT + $1 as symbol, + (EXTRACT(EPOCH FROM time_bucket('{interval}', to_timestamp(timestamp/1000.0))) * 1000)::bigint as bucket_ts, + (array_agg(open ORDER BY timestamp ASC))[1] as open, + max(high) as high, + min(low) as low, + (array_agg(close ORDER BY timestamp DESC))[1] as close, + sum(volume) as volume, + sum(taker_buy_volume) as taker_buy_volume, + sum(net_volume) as net_volume + FROM klines_1m + WHERE symbol = $1 AND timestamp >= $3 AND timestamp <= $4 + GROUP BY bucket_ts + ORDER BY bucket_ts DESC + LIMIT $2 + "#, + interval = interval_str, + ); + + let rows: Vec<(String, i64, f64, f64, f64, f64, f64, f64, f64)> = sqlx::query_as(&query) + .bind(&symbol_upper) + .bind(limit) + .bind(start_time) + .bind(et) + .fetch_all(&self.pool) + .await?; + + let mut candles: Vec = rows.into_iter().map(|r| CandleData { + symbol: r.0, + timestamp: r.1, + open: r.2, + high: r.3, + low: r.4, + close: r.5, + volume: r.6, + taker_buy_volume: r.7, + net_volume: r.8, + is_closed: true, + }).collect(); + + // Reverse to chronological order (oldest first) + candles.reverse(); + + Ok(candles) + } + + /// Find gaps in 1-minute kline data for a symbol. + /// Returns a list of (start_timestamp, end_timestamp) pairs representing gaps. + /// Each gap represents missing data from start_timestamp to end_timestamp (exclusive). + /// Only considers data after DATA_CUTOFF_TIMESTAMP (2024-01-01). + pub async fn find_gaps(&self, symbol: &str, cutoff_timestamp: i64) -> Result, sqlx::Error> { + let symbol_upper = symbol.to_uppercase(); + + // Query to find gaps using window function + // We look for cases where the next timestamp is more than 1 minute away + // Only consider data after cutoff_timestamp + let rows: Vec<(i64, i64)> = sqlx::query_as( + r#" + WITH ordered_klines AS ( + SELECT timestamp, + LEAD(timestamp) OVER (ORDER BY timestamp) as next_timestamp + FROM klines_1m + WHERE symbol = $1 AND timestamp >= $2 + ) + SELECT timestamp + 60000 as gap_start, next_timestamp as gap_end + FROM ordered_klines + WHERE next_timestamp - timestamp > 60000 + ORDER BY timestamp + "# + ) + .bind(&symbol_upper) + .bind(cutoff_timestamp) + .fetch_all(&self.pool) + .await?; + + Ok(rows) + } + + /// Get the earliest timestamp for a symbol + pub async fn get_earliest_timestamp(&self, symbol: &str) -> Result, sqlx::Error> { + let row: Option<(Option,)> = sqlx::query_as( + "SELECT MIN(timestamp) FROM klines_1m WHERE symbol = $1" + ) + .bind(symbol.to_uppercase()) + .fetch_optional(&self.pool) + .await?; + + Ok(row.and_then(|r| r.0)) + } + + /// Get count of klines for a symbol + pub async fn get_kline_count(&self, symbol: &str) -> Result { + let row: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM klines_1m WHERE symbol = $1" + ) + .bind(symbol.to_uppercase()) + .fetch_one(&self.pool) + .await?; + + Ok(row.0) + } +} diff --git a/backend/src/error.rs b/backend/src/error.rs new file mode 100644 index 0000000..5abf77a --- /dev/null +++ b/backend/src/error.rs @@ -0,0 +1,47 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CollectorError { + #[error("WebSocket connection failed: {0}")] + ConnectionFailed(String), + + #[error("REST API request failed: {0}")] + RestApiError(String), + + #[error("Data parsing error: {0}")] + ParseError(String), + + #[error("Invalid kline data: {0}")] + InvalidKlineData(String), + + #[error("Failed to get opened symbol, with error {0}")] + GetSymbolError(String), + +} + +#[derive(Error, Debug)] +pub enum SchedulerError { + #[error("Database error: {0}")] + DatabaseError(String), + + #[error("Collector error: {0}")] + CollectorError(String), + + #[error("Backfill error: {0}")] + BackfillError(String), + + #[error("No active symbols")] + NoActiveSymbols, +} + +impl From for SchedulerError { + fn from(err: sqlx::Error) -> Self { + SchedulerError::DatabaseError(err.to_string()) + } +} + +impl From for SchedulerError { + fn from(err: CollectorError) -> Self { + SchedulerError::CollectorError(err.to_string()) + } +} diff --git a/backend/src/historical_downloader.rs b/backend/src/historical_downloader.rs new file mode 100644 index 0000000..9c7d6d4 --- /dev/null +++ b/backend/src/historical_downloader.rs @@ -0,0 +1,522 @@ +use crate::structs::CandleData; +use crate::error::CollectorError; + +use chrono::{Datelike, NaiveDate, Utc}; +use log::{info, warn}; +use std::io::{Cursor, Read}; +use std::sync::Arc; +use tokio::sync::mpsc; +use futures::future::join_all; +use reqwest::Client; + +const BASE_URL: &str = "https://data.binance.vision/data/futures/um/monthly/klines"; +const DAILY_BASE_URL: &str = "https://data.binance.vision/data/futures/um/daily/klines"; + +/// Download historical klines from Binance data archive +pub struct HistoricalDownloader; + +impl HistoricalDownloader { + /// Build proxy clients from env vars (PROXY_HOST, PROXY_USERNAME, PROXY_PASSWORD, PROXY_PROTOCOL) + /// Port range defaults to PROXY_PORT_START..PROXY_PORT_END (default 10000..10099) + /// Returns direct (no-proxy) clients if PROXY_HOST is not set. + pub async fn build_clients() -> Vec> { + let mut handles = Vec::new(); + + let proxy_host = std::env::var("PROXY_HOST").unwrap_or_default(); + let proxy_username = std::env::var("PROXY_USERNAME").unwrap_or_default(); + let proxy_password = std::env::var("PROXY_PASSWORD").unwrap_or_default(); + let proxy_protocol = std::env::var("PROXY_PROTOCOL").unwrap_or_else(|_| "https".to_string()); + let port_start: u16 = std::env::var("PROXY_PORT_START") + .ok().and_then(|v| v.parse().ok()).unwrap_or(10000); + let port_end: u16 = std::env::var("PROXY_PORT_END") + .ok().and_then(|v| v.parse().ok()).unwrap_or(10099); + + if proxy_host.is_empty() { + // No proxy configured — return a single direct client + info!("No proxy configured (PROXY_HOST not set), using direct connection"); + if let Ok(client) = Client::builder() + .timeout(std::time::Duration::from_secs(60)) + .build() + { + return vec![Arc::new(client)]; + } + return vec![]; + } + + for port in port_start..=port_end { + let host = proxy_host.clone(); + let user = proxy_username.clone(); + let pass = proxy_password.clone(); + let proto = proxy_protocol.clone(); + + handles.push(tokio::spawn(async move { + let proxy_url = if user.is_empty() { + format!("{}://{}:{}", proto, host, port) + } else { + format!("{}://{}:{}@{}:{}", proto, user, pass, host, port) + }; + + match reqwest::Proxy::all(&proxy_url) { + Ok(proxy) => { + match Client::builder() + .proxy(proxy) + .timeout(std::time::Duration::from_secs(60)) + .build() + { + Ok(client) => Some(Arc::new(client)), + Err(_) => None, + } + } + Err(_) => None, + } + })); + } + + let mut clients = Vec::new(); + for handle in handles { + if let Ok(Some(client)) = handle.await { + clients.push(client); + } + } + + info!("Built {} download clients with proxies", clients.len()); + clients + } + + /// Download all monthly klines for a symbol using proxy clients + pub async fn download_symbol_with_clients( + symbol: &str, + start_timestamp: i64, + clients: &[Arc], + candle_tx: mpsc::Sender, + ) -> Result { + let symbol_upper = symbol.to_uppercase(); + + if clients.is_empty() { + return Err(CollectorError::RestApiError("No clients available".to_string())); + } + + // Calculate start and end months + let start_date = Self::timestamp_to_date(start_timestamp); + let now = Utc::now().naive_utc().date(); + + // Generate list of months to download (exclude current month - incomplete) + let months = Self::generate_months(start_date, now); + + if months.is_empty() { + info!("{}: No complete months to download", symbol_upper); + return Ok(0); + } + + info!( + "{}: Downloading {} months ({}-{:02} to {}-{:02}) with {} clients", + symbol_upper, + months.len(), + months.first().unwrap().0, months.first().unwrap().1, + months.last().unwrap().0, months.last().unwrap().1, + clients.len() + ); + + // Download all months in parallel using all clients + let mut handles = Vec::new(); + + for (i, &(year, month)) in months.iter().enumerate() { + let client = clients[i % clients.len()].clone(); + let sym = symbol_upper.clone(); + let tx = candle_tx.clone(); + + handles.push(tokio::spawn(async move { + Self::download_month_with_client(&sym, year, month, &client, tx).await + })); + } + + // Wait for all downloads + let results = join_all(handles).await; + + let mut total_candles = 0u64; + for result in results { + match result { + Ok(Ok(count)) => total_candles += count, + Ok(Err(e)) => warn!("Download error: {}", e), + Err(e) => warn!("Join error: {}", e), + } + } + + info!("{}: Downloaded {} candles from archive", symbol_upper, total_candles); + Ok(total_candles) + } + + /// Download a single month's klines using a specific client + async fn download_month_with_client( + symbol: &str, + year: i32, + month: u32, + client: &Client, + candle_tx: mpsc::Sender, + ) -> Result { + let url = format!( + "{}/{}/1m/{}-1m-{}-{:02}.zip", + BASE_URL, symbol, symbol, year, month + ); + + // Download ZIP file with proxy client + let response = client.get(&url) + .send() + .await + .map_err(|e| CollectorError::RestApiError(format!("Download failed: {}", e)))?; + + if !response.status().is_success() { + return Err(CollectorError::RestApiError( + format!("HTTP {}: {}", response.status(), url) + )); + } + + let bytes = response.bytes() + .await + .map_err(|e| CollectorError::RestApiError(format!("Read bytes failed: {}", e)))?; + + // Extract CSV from ZIP + let cursor = Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(cursor) + .map_err(|e| CollectorError::RestApiError(format!("ZIP error: {}", e)))?; + + let mut csv_content = String::new(); + { + let mut file = archive.by_index(0) + .map_err(|e| CollectorError::RestApiError(format!("ZIP file error: {}", e)))?; + file.read_to_string(&mut csv_content) + .map_err(|e| CollectorError::RestApiError(format!("Read CSV error: {}", e)))?; + } + + // Parse CSV and send candles + let count = Self::parse_csv(symbol, &csv_content, &candle_tx).await?; + + info!("{} {}-{:02}: {} candles", symbol, year, month, count); + Ok(count) + } + + /// Download all monthly klines (without proxy - original method) + pub async fn download_symbol( + symbol: &str, + start_timestamp: i64, + candle_tx: mpsc::Sender, + max_parallel: usize, + ) -> Result { + let symbol_upper = symbol.to_uppercase(); + + let start_date = Self::timestamp_to_date(start_timestamp); + let now = Utc::now().naive_utc().date(); + let months = Self::generate_months(start_date, now); + + if months.is_empty() { + info!("{}: No complete months to download", symbol_upper); + return Ok(0); + } + + info!( + "{}: Downloading {} months from {}-{:02} to {}-{:02}", + symbol_upper, + months.len(), + months.first().unwrap().0, months.first().unwrap().1, + months.last().unwrap().0, months.last().unwrap().1 + ); + + let mut total_candles = 0u64; + + for chunk in months.chunks(max_parallel) { + let mut handles = Vec::new(); + + for &(year, month) in chunk { + let sym = symbol_upper.clone(); + let tx = candle_tx.clone(); + + handles.push(tokio::spawn(async move { + Self::download_month(&sym, year, month, tx).await + })); + } + + let results = join_all(handles).await; + + for result in results { + match result { + Ok(Ok(count)) => total_candles += count, + Ok(Err(e)) => warn!("Download error: {}", e), + Err(e) => warn!("Join error: {}", e), + } + } + } + + info!("{}: Downloaded {} candles from archive", symbol_upper, total_candles); + Ok(total_candles) + } + + /// Download a single month (without proxy) + async fn download_month( + symbol: &str, + year: i32, + month: u32, + candle_tx: mpsc::Sender, + ) -> Result { + let url = format!( + "{}/{}/1m/{}-1m-{}-{:02}.zip", + BASE_URL, symbol, symbol, year, month + ); + + let response = reqwest::get(&url) + .await + .map_err(|e| CollectorError::RestApiError(format!("Download failed: {}", e)))?; + + if !response.status().is_success() { + return Err(CollectorError::RestApiError( + format!("HTTP {}: {}", response.status(), url) + )); + } + + let bytes = response.bytes() + .await + .map_err(|e| CollectorError::RestApiError(format!("Read bytes failed: {}", e)))?; + + let cursor = Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(cursor) + .map_err(|e| CollectorError::RestApiError(format!("ZIP error: {}", e)))?; + + let mut csv_content = String::new(); + { + let mut file = archive.by_index(0) + .map_err(|e| CollectorError::RestApiError(format!("ZIP file error: {}", e)))?; + file.read_to_string(&mut csv_content) + .map_err(|e| CollectorError::RestApiError(format!("Read CSV error: {}", e)))?; + } + + let count = Self::parse_csv(symbol, &csv_content, &candle_tx).await?; + + info!("{} {}-{:02}: {} candles", symbol, year, month, count); + Ok(count) + } + + /// Parse CSV content and send candles through channel + async fn parse_csv( + symbol: &str, + csv_content: &str, + candle_tx: &mpsc::Sender, + ) -> Result { + let mut reader = csv::ReaderBuilder::new() + .has_headers(true) + .from_reader(csv_content.as_bytes()); + + let mut count = 0u64; + + for result in reader.records() { + let record = match result { + Ok(r) => r, + Err(_) => continue, + }; + + if record.len() < 11 { + continue; + } + + let timestamp: i64 = record[0].parse().unwrap_or(0); + let open: f64 = record[1].parse().unwrap_or(0.0); + let high: f64 = record[2].parse().unwrap_or(0.0); + let low: f64 = record[3].parse().unwrap_or(0.0); + let close: f64 = record[4].parse().unwrap_or(0.0); + let volume: f64 = record[5].parse().unwrap_or(0.0); + let taker_buy_volume: f64 = record[9].parse().unwrap_or(0.0); + + let net_volume = taker_buy_volume * 2.0 - volume; + + let candle = CandleData { + symbol: symbol.to_string(), + timestamp, + open, + high, + low, + close, + volume, + taker_buy_volume, + net_volume, + is_closed: true, + }; + + if candle_tx.send(candle).await.is_err() { + break; + } + count += 1; + } + + Ok(count) + } + + fn timestamp_to_date(ts: i64) -> NaiveDate { + let secs = ts / 1000; + chrono::DateTime::from_timestamp(secs, 0) + .unwrap_or_else(|| Utc::now()) + .naive_utc() + .date() + } + + fn generate_months(start: NaiveDate, end: NaiveDate) -> Vec<(i32, u32)> { + let mut months = Vec::new(); + + let mut year = start.year(); + let mut month = start.month(); + + let end_year = if end.month() == 1 { end.year() - 1 } else { end.year() }; + let end_month = if end.month() == 1 { 12 } else { end.month() - 1 }; + + loop { + if year > end_year || (year == end_year && month > end_month) { + break; + } + + months.push((year, month)); + + month += 1; + if month > 12 { + month = 1; + year += 1; + } + } + + months + } + + pub fn last_complete_month_end() -> i64 { + let now = Utc::now().naive_utc(); + let first_of_this_month = NaiveDate::from_ymd_opt(now.year(), now.month(), 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + first_of_this_month.and_utc().timestamp_millis() + } + + /// Download daily klines for a symbol (for current incomplete month) + pub async fn download_days_with_clients( + symbol: &str, + start_timestamp: i64, + clients: &[Arc], + candle_tx: mpsc::Sender, + ) -> Result { + let symbol_upper = symbol.to_uppercase(); + + if clients.is_empty() { + return Err(CollectorError::RestApiError("No clients available".to_string())); + } + + let start_date = Self::timestamp_to_date(start_timestamp); + let yesterday = Utc::now().naive_utc().date().pred_opt().unwrap_or(start_date); + + let days = Self::generate_days(start_date, yesterday); + + if days.is_empty() { + info!("{}: No days to download", symbol_upper); + return Ok(0); + } + + info!( + "{}: Downloading {} days ({} to {}) with {} clients", + symbol_upper, days.len(), + days.first().unwrap(), days.last().unwrap(), + clients.len() + ); + + let mut handles = Vec::new(); + + for (i, date) in days.iter().enumerate() { + let client = clients[i % clients.len()].clone(); + let sym = symbol_upper.clone(); + let tx = candle_tx.clone(); + let d = *date; + + handles.push(tokio::spawn(async move { + Self::download_day_with_client(&sym, d, &client, tx).await + })); + } + + let results = join_all(handles).await; + + let mut total_candles = 0u64; + for result in results { + match result { + Ok(Ok(count)) => total_candles += count, + Ok(Err(e)) => warn!("Daily download error: {}", e), + Err(e) => warn!("Join error: {}", e), + } + } + + info!("{}: Downloaded {} candles from daily archive", symbol_upper, total_candles); + Ok(total_candles) + } + + /// Download a single day's klines + async fn download_day_with_client( + symbol: &str, + date: NaiveDate, + client: &Client, + candle_tx: mpsc::Sender, + ) -> Result { + let url = format!( + "{}/{}/1m/{}-1m-{}.zip", + DAILY_BASE_URL, symbol, symbol, date.format("%Y-%m-%d") + ); + + let response = client.get(&url) + .send() + .await + .map_err(|e| CollectorError::RestApiError(format!("Download failed: {}", e)))?; + + if !response.status().is_success() { + return Err(CollectorError::RestApiError( + format!("HTTP {}: {}", response.status(), url) + )); + } + + let bytes = response.bytes() + .await + .map_err(|e| CollectorError::RestApiError(format!("Read bytes failed: {}", e)))?; + + let cursor = Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(cursor) + .map_err(|e| CollectorError::RestApiError(format!("ZIP error: {}", e)))?; + + let mut csv_content = String::new(); + { + let mut file = archive.by_index(0) + .map_err(|e| CollectorError::RestApiError(format!("ZIP file error: {}", e)))?; + file.read_to_string(&mut csv_content) + .map_err(|e| CollectorError::RestApiError(format!("Read CSV error: {}", e)))?; + } + + let count = Self::parse_csv(symbol, &csv_content, &candle_tx).await?; + + info!("{} {}: {} candles", symbol, date, count); + Ok(count) + } + + /// Generate list of days between start and end (inclusive) + fn generate_days(start: NaiveDate, end: NaiveDate) -> Vec { + let mut days = Vec::new(); + let mut current = start; + + while current <= end { + days.push(current); + current = current.succ_opt().unwrap_or(end); + if current == end && days.last() == Some(&end) { + break; + } + } + + days + } + + /// Get timestamp for start of current month (for daily download start point) + pub fn current_month_start() -> i64 { + let now = Utc::now().naive_utc(); + let first_of_this_month = NaiveDate::from_ymd_opt(now.year(), now.month(), 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + first_of_this_month.and_utc().timestamp_millis() + } +} diff --git a/backend/src/klinechart.rs b/backend/src/klinechart.rs new file mode 100644 index 0000000..b08bc3c --- /dev/null +++ b/backend/src/klinechart.rs @@ -0,0 +1,103 @@ +use crate::{ + DatabaseHandler, SchedulerCommand, + CandleData, KlineQuery, ApiResponse, AddSymbolRequest, SchedulerStatus, +}; + +use axum::{ + Router, + routing::{get, post, delete}, + extract::{Path, Query, State}, + response::Json, + http::StatusCode, +}; +use std::sync::Arc; +use tokio::sync::{mpsc, oneshot}; + +pub struct KlineChartState { + pub db: Arc, + pub command_tx: mpsc::Sender, +} + +pub fn klinechart_routes() -> Router> { + Router::new() + .route("/api/klines/{symbol}", get(get_klines)) + .route("/api/symbols", get(get_symbols)) + .route("/api/symbols", post(add_symbol)) + .route("/api/symbols/{symbol}", delete(remove_symbol)) + .route("/api/status", get(get_status)) +} + +// GET /api/klines/{symbol}?limit=800&interval=1m&end_time=... +async fn get_klines( + State(state): State>, + Path(symbol): Path, + Query(query): Query, +) -> Json>> { + let limit = query.limit.unwrap_or(800); + let interval = query.interval.unwrap_or_default(); + let end_time = query.end_time; + + match state.db.get_klines_aggregated(&symbol, interval, limit, end_time).await { + Ok(candles) => Json(ApiResponse::ok(candles)), + Err(e) => Json(ApiResponse::err(&e.to_string())), + } +} + +// GET /api/symbols +async fn get_symbols( + State(state): State>, +) -> Json>> { + match state.db.get_active_symbols().await { + Ok(symbols) => Json(ApiResponse::ok(symbols)), + Err(e) => Json(ApiResponse::err(&e.to_string())), + } +} + +// POST /api/symbols +async fn add_symbol( + State(state): State>, + Json(req): Json, +) -> (StatusCode, Json>) { + let result = state.command_tx.send(SchedulerCommand::AddSymbol { + symbol: req.symbol.clone(), + backfill_from: req.backfill_from, + }).await; + + match result { + Ok(_) => (StatusCode::OK, Json(ApiResponse::ok(format!("Adding symbol: {}", req.symbol)))), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::err(&e.to_string()))), + } +} + +// DELETE /api/symbols/{symbol} +async fn remove_symbol( + State(state): State>, + Path(symbol): Path, +) -> (StatusCode, Json>) { + let result = state.command_tx.send(SchedulerCommand::RemoveSymbol { + symbol: symbol.clone(), + }).await; + + match result { + Ok(_) => (StatusCode::OK, Json(ApiResponse::ok(format!("Removing symbol: {}", symbol)))), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::err(&e.to_string()))), + } +} + +// GET /api/status +async fn get_status( + State(state): State>, +) -> Json> { + let (reply_tx, reply_rx) = oneshot::channel(); + + let send_result = state.command_tx.send(SchedulerCommand::GetStatus { reply: reply_tx }).await; + + if send_result.is_err() { + return Json(ApiResponse::err("Failed to send status request")); + } + + match reply_rx.await { + Ok(status) => Json(ApiResponse::ok(status)), + Err(_) => Json(ApiResponse::err("Failed to receive status")), + } +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 0000000..f353f22 --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,23 @@ +pub mod structs; +pub use structs::*; + +pub mod error; +pub use error::*; + +pub mod binance_collector; +pub use binance_collector::*; + +pub mod database; +pub use database::*; + +pub mod scheduler; +pub use scheduler::*; + +pub mod klinechart; +pub use klinechart::*; + +pub mod tradingview; +pub use tradingview::*; + +pub mod historical_downloader; +pub use historical_downloader::*; \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..8f8bfb1 --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,83 @@ +use backend::{ + DatabaseHandler, Scheduler, create_command_channel, + KlineChartState, klinechart_routes, + TradingViewState, tradingview_routes, + CandleData, +}; + +use axum::Router; +use tower_http::cors::{CorsLayer, Any}; +use axum::http::Method; +use std::sync::Arc; +use log::info; +use dotenv::*; +use tokio::sync::mpsc; + +#[tokio::main] +async fn main() { + + dotenv().ok(); + env_logger::init(); + + info!("Current Version 1.4"); + + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://quant:2Nr!Ya&oVvY5pp@172.18.0.2:5432/crypto_database".to_string()); + + info!("Connecting to database..."); + let db = Arc::new( + DatabaseHandler::new(&database_url) + .await + .expect("Failed to connect to database") + ); + + let (command_tx, command_rx) = create_command_channel(); + + // Create TradingView state with broadcast channel + let (tradingview_state, _) = TradingViewState::new(db.clone()); + let tradingview_state = Arc::new(tradingview_state); + + // Create channel for WebSocket broadcasts + let (ws_broadcast_tx, mut ws_broadcast_rx) = mpsc::channel::(10000); + + // Forward candles to TradingView broadcast + let tv_state_clone = tradingview_state.clone(); + tokio::spawn(async move { + while let Some(candle) = ws_broadcast_rx.recv().await { + tv_state_clone.broadcast_candle(candle); + } + }); + + // Start scheduler in background with ws_broadcast_tx + let scheduler_db = db.clone(); + tokio::spawn(async move { + let mut scheduler = Scheduler::new(scheduler_db, command_rx, Some(ws_broadcast_tx)); + scheduler.run().await; + }); + + // KlineChart state (for klinechart frontend) + let klinechart_state = Arc::new(KlineChartState { + db: db.clone(), + command_tx, + }); + + // CORS for frontend + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS]) + .allow_headers(Any); + + // Merge routes from both modules + let app = Router::new() + .merge(klinechart_routes().with_state(klinechart_state)) + .merge(tradingview_routes().with_state(tradingview_state)) + .layer(cors); + + let addr = "0.0.0.0:3000"; + info!("Starting API server on {}", addr); + info!("KlineChart API: /api/klines, /api/symbols, /api/status"); + info!("TradingView UDF: /config, /symbols, /search, /history, /time"); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/backend/src/scheduler.rs b/backend/src/scheduler.rs new file mode 100644 index 0000000..4508b85 --- /dev/null +++ b/backend/src/scheduler.rs @@ -0,0 +1,320 @@ +use crate::database::DatabaseHandler; +use crate::binance_collector::BinanceCollector; +use crate::historical_downloader::HistoricalDownloader; +use crate::error::SchedulerError; +use crate::structs::*; +use crate::DATA_CUTOFF_TIMESTAMP; + +use log::{info, error, warn}; +use std::sync::Arc; +use tokio::sync::{mpsc, RwLock}; +use tokio::task::JoinHandle; + +const ONE_MINUTE_MS: i64 = 60_000; + +pub struct Scheduler { + db: Arc, + command_rx: mpsc::Receiver, + collector_handle: Option>, + is_running: Arc>, + active_symbols: Arc>>, + ws_broadcast_tx: Option>, +} + +impl Scheduler { + pub fn new( + db: Arc, + command_rx: mpsc::Receiver, + ws_broadcast_tx: Option>, + ) -> Self { + Self { + db, + command_rx, + collector_handle: None, + is_running: Arc::new(RwLock::new(false)), + active_symbols: Arc::new(RwLock::new(Vec::new())), + ws_broadcast_tx, + } + } + + pub async fn run(&mut self) { + info!("Scheduler started"); + + // Initial startup + if let Err(e) = self.start_collector().await { + error!("Failed to start collector on init: {}", e); + } + + // Command processing loop + while let Some(cmd) = self.command_rx.recv().await { + match cmd { + SchedulerCommand::AddSymbol { symbol, backfill_from: _ } => { + info!("AddSymbol command ignored - symbols managed by get_symbol()"); + let _ = self.db.add_symbol(&symbol).await; + } + SchedulerCommand::RemoveSymbol { symbol } => { + info!("RemoveSymbol: {}", symbol); + let _ = self.db.remove_symbol(&symbol).await; + } + SchedulerCommand::RestartCollector => { + self.handle_restart_collector().await; + } + SchedulerCommand::GetStatus { reply } => { + self.handle_get_status(reply).await; + } + SchedulerCommand::Shutdown => { + info!("Shutdown command received"); + self.stop_collector().await; + break; + } + } + } + + info!("Scheduler stopped"); + } + + async fn handle_restart_collector(&mut self) { + info!("Restarting collector..."); + self.stop_collector().await; + if let Err(e) = self.start_collector().await { + error!("Failed to restart collector: {}", e); + } + } + + async fn handle_get_status(&self, reply: tokio::sync::oneshot::Sender) { + let is_running = *self.is_running.read().await; + let active_symbols = self.active_symbols.read().await.clone(); + let collector_connected = self.collector_handle.is_some() && is_running; + + let _ = reply.send(SchedulerStatus { + is_running, + active_symbols, + collector_connected, + }); + } + + async fn start_collector(&mut self) -> Result<(), SchedulerError> { + // STEP 1: Get symbols from TRACKED_SYMBOLS env variable + let symbol_names = DatabaseHandler::get_symbols_from_env(); + + if symbol_names.is_empty() { + warn!("No symbols in TRACKED_SYMBOLS env variable. Set TRACKED_SYMBOLS=BTCUSDT,ETHUSDT,..."); + return Ok(()); + } + info!("Tracking {} symbols from env: {:?}", symbol_names.len(), symbol_names); + + // Update active symbols + { + let mut active = self.active_symbols.write().await; + *active = symbol_names.clone(); + } + + // STEP 2: Start WebSocket FIRST to capture real-time data immediately + info!("Starting WebSocket collector FIRST (priority: real-time data)..."); + let collector = BinanceCollector::new(symbol_names.clone()); + + let (candle_tx, mut candle_rx) = mpsc::channel::(10000); + + // Consumer that both saves to DB and broadcasts to WebSocket clients + let db = self.db.clone(); + let ws_tx = self.ws_broadcast_tx.clone(); + tokio::spawn(async move { + while let Some(candle) = candle_rx.recv().await { + // Broadcast to WebSocket clients immediately (all updates for real-time charts) + if let Some(ref tx) = ws_tx { + let _ = tx.send(candle.clone()).await; + } + + // Only save closed candles to database + if candle.is_closed { + if let Err(e) = db.insert_candle(&candle).await { + error!("Failed to insert realtime candle: {}", e); + } + } + } + }); + + let is_running = self.is_running.clone(); + let handle = tokio::spawn(async move { + { + let mut running = is_running.write().await; + *running = true; + } + + if let Err(e) = collector.start_stream(candle_tx).await { + error!("Collector error: {}", e); + } + + { + let mut running = is_running.write().await; + *running = false; + } + }); + + self.collector_handle = Some(handle); + info!("WebSocket collector started! Real-time data is now being captured."); + + // STEP 3: Background sync - runs in parallel with WebSocket + // This fills in historical data without blocking real-time updates + let db_for_sync = self.db.clone(); + let symbols_for_sync = symbol_names.clone(); + + tokio::spawn(async move { + info!("Starting background historical sync..."); + + // Build API clients for parallel sync + let api_clients = BinanceCollector::build_clients().await; + if api_clients.is_empty() { + error!("No working API clients available for background sync"); + return; + } + info!("Built {} API clients for background sync", api_clients.len()); + + // Build HTTP clients for ZIP downloads + let http_clients = HistoricalDownloader::build_clients().await; + info!("Built {} HTTP clients for ZIP downloads", http_clients.len()); + + // Create channel for background sync (lower priority, uses batching) + let (sync_tx, sync_rx) = mpsc::channel::(100000); + + let db_consumer = db_for_sync.clone(); + let consumer_handle = tokio::spawn(async move { + db_consumer.start_consumer(sync_rx).await; + }); + + // Sync symbols that need updates + let mut synced_count = 0; + let total_symbols = symbols_for_sync.len(); + + for symbol in &symbols_for_sync { + let latest_ts = db_for_sync.get_latest_timestamp(symbol).await.ok().flatten(); + + let start_time = match latest_ts { + Some(ts) => ts + ONE_MINUTE_MS, + None => DATA_CUTOFF_TIMESTAMP, + }; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + // Skip if less than 2 minutes behind (WebSocket will catch up) + if now - start_time < ONE_MINUTE_MS * 2 { + synced_count += 1; + continue; + } + + let behind_mins = (now - start_time) / ONE_MINUTE_MS; + info!("[{}/{}] Syncing {} ({} minutes behind)...", + synced_count + 1, total_symbols, symbol, behind_mins); + + // Use comprehensive sync: Monthly ZIP → Daily ZIP → API + match BinanceCollector::sync_comprehensive( + symbol.clone(), + start_time, + api_clients.clone(), + &http_clients, + sync_tx.clone(), + ).await { + Ok(count) => { + synced_count += 1; + if count > 0 { + info!("[{}/{}] {} synced: {} candles", synced_count, total_symbols, symbol, count); + } + } + Err(e) => { + synced_count += 1; + error!("{} sync failed: {}", symbol, e); + } + } + } + + // Close sync channel + drop(sync_tx); + let _ = consumer_handle.await; + info!("Background historical sync complete! Synced {} symbols", synced_count); + + // STEP 4: Gap detection and repair (also in background) + info!("Starting background gap detection and repair..."); + let (gap_tx, gap_rx) = mpsc::channel::(100000); + + let db_gap = db_for_sync.clone(); + let gap_consumer_handle = tokio::spawn(async move { + db_gap.start_consumer(gap_rx).await; + }); + + // Rebuild clients for gap repair + let clients = BinanceCollector::build_clients().await; + let mut total_gaps_repaired = 0u64; + let mut symbols_with_gaps = 0; + + for symbol in &symbols_for_sync { + match db_for_sync.find_gaps(symbol, DATA_CUTOFF_TIMESTAMP).await { + Ok(gaps) => { + if gaps.is_empty() { + continue; + } + + symbols_with_gaps += 1; + info!("{} - Found {} gaps to repair", symbol, gaps.len()); + + for (gap_start, gap_end) in gaps { + let gap_duration_mins = (gap_end - gap_start) / ONE_MINUTE_MS; + + // Skip very small gaps (less than 2 minutes) + if gap_duration_mins < 2 { + continue; + } + + info!( + "{} - Repairing gap: {} minutes", + symbol, gap_duration_mins + ); + + match BinanceCollector::sync_from_scratch( + symbol.clone(), + gap_start, + clients.clone(), + gap_tx.clone(), + ).await { + Ok(count) => { + total_gaps_repaired += count; + } + Err(e) => { + error!("{} - Gap repair failed: {}", symbol, e); + } + } + } + } + Err(e) => { + warn!("{} - Failed to check gaps: {}", symbol, e); + } + } + } + + // Close gap repair channel + drop(gap_tx); + let _ = gap_consumer_handle.await; + info!("Gap detection complete! {} symbols had gaps, repaired {} candles total", + symbols_with_gaps, total_gaps_repaired); + }); + + Ok(()) + } + + async fn stop_collector(&mut self) { + if let Some(handle) = self.collector_handle.take() { + info!("Stopping collector..."); + handle.abort(); + let _ = handle.await; + + let mut running = self.is_running.write().await; + *running = false; + } + } +} + +pub fn create_command_channel() -> (mpsc::Sender, mpsc::Receiver) { + mpsc::channel(100) +} diff --git a/backend/src/structs.rs b/backend/src/structs.rs new file mode 100644 index 0000000..63e19a6 --- /dev/null +++ b/backend/src/structs.rs @@ -0,0 +1,159 @@ +use serde::{Serialize, Deserialize}; +use tokio::sync::oneshot; + +/// Data cutoff timestamp: 2024-01-01 00:00:00 UTC +/// Data before this timestamp will not be synced +pub const DATA_CUTOFF_TIMESTAMP: i64 = 1704067200000; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CandleData { + pub symbol: String, + pub timestamp: i64, + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub volume: f64, + pub taker_buy_volume: f64, + pub net_volume: f64, + pub is_closed: bool, +} + +impl CandleData { + pub fn calculate_net_volume(total_volume: f64, taker_buy_volume: f64) -> f64 { + 2.0 * taker_buy_volume - total_volume + } +} + +/// Commands for controlling the Scheduler +pub enum SchedulerCommand { + /// Add a new symbol to track. Triggers backfill then collector restart. + AddSymbol { + symbol: String, + backfill_from: Option, // None = use EARLIEST_TIME + }, + + /// Remove a symbol from tracking. Triggers collector restart. + RemoveSymbol { symbol: String }, + + /// Restart the collector with current active symbols from database + RestartCollector, + + /// Get current scheduler status + GetStatus { reply: oneshot::Sender }, + + /// Graceful shutdown + Shutdown, +} + +/// Scheduler status for API responses +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchedulerStatus { + pub is_running: bool, + pub active_symbols: Vec, + pub collector_connected: bool, +} + +// API Request/Response structs + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Interval { + #[serde(rename = "1m")] + Min1, + #[serde(rename = "5m")] + Min5, + #[serde(rename = "15m")] + Min15, + #[serde(rename = "1h")] + Hour1, + #[serde(rename = "4h")] + Hour4, + #[serde(rename = "1d")] + Day1, + #[serde(rename = "1w")] + Week1, + #[serde(rename = "1M")] + Month1, +} + +impl Default for Interval { + fn default() -> Self { + Interval::Min1 + } +} + +#[derive(Deserialize)] +pub struct KlineQuery { + pub limit: Option, + pub interval: Option, + pub end_time: Option, +} + +#[derive(Serialize)] +pub struct ApiResponse { + pub success: bool, + pub data: Option, + pub error: Option, +} + +impl ApiResponse { + pub fn ok(data: T) -> Self { + Self { success: true, data: Some(data), error: None } + } + + pub fn err(msg: &str) -> Self { + Self { success: false, data: None, error: Some(msg.to_string()) } + } +} + +#[derive(Deserialize)] +pub struct AddSymbolRequest { + pub symbol: String, + pub backfill_from: Option, +} + +#[derive(Serialize,Deserialize,Debug,Clone)] +pub struct Symbol{ + + pub symbol: String, + pub start_timestamp: i64, +} + +// WebSocket message types +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum WsMessage { + /// Kline update for a symbol + #[serde(rename = "kline")] + Kline(CandleData), + + /// Ticker update with price info for watchlist + #[serde(rename = "ticker")] + Ticker(TickerUpdate), + + /// Subscribe to symbols + #[serde(rename = "subscribe")] + Subscribe { symbols: Vec }, + + /// Unsubscribe from symbols + #[serde(rename = "unsubscribe")] + Unsubscribe { symbols: Vec }, + + /// Ping/Pong for keepalive + #[serde(rename = "ping")] + Ping, + + #[serde(rename = "pong")] + Pong, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TickerUpdate { + pub symbol: String, + pub price: f64, + pub change_24h: f64, + pub change_percent_24h: f64, + pub volume_24h: f64, + pub timestamp: i64, +} diff --git a/backend/src/tradingview.rs b/backend/src/tradingview.rs new file mode 100644 index 0000000..5aa71db --- /dev/null +++ b/backend/src/tradingview.rs @@ -0,0 +1,559 @@ +use crate::{DatabaseHandler, Interval, CandleData, WsMessage}; + +use axum::{ + Router, + routing::{get, post, delete}, + extract::{Query, State, WebSocketUpgrade, ws::{Message, WebSocket}}, + response::{Json, IntoResponse}, + http::StatusCode, +}; +use futures::{StreamExt, SinkExt}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::path::PathBuf; +use tokio::sync::{broadcast, RwLock}; +use tokio::fs; +use log::{info, warn, debug, error}; + +pub struct TradingViewState { + pub db: Arc, + pub candle_tx: broadcast::Sender, +} + +impl TradingViewState { + pub fn new(db: Arc) -> (Self, broadcast::Receiver) { + let (candle_tx, candle_rx) = broadcast::channel(10000); + (Self { db, candle_tx }, candle_rx) + } + + /// Send a candle update to all connected WebSocket clients + pub fn broadcast_candle(&self, candle: CandleData) { + // Ignore errors (no subscribers) + let _ = self.candle_tx.send(candle); + } +} + +pub fn tradingview_routes() -> Router> { + Router::new() + .route("/config", get(get_config)) + .route("/time", get(get_time)) + .route("/symbols", get(get_symbol_info)) + .route("/search", get(search_symbols)) + .route("/tracked-symbols", get(get_tracked_symbols)) + .route("/daily-opens", get(get_daily_opens)) + .route("/history", get(get_history)) + .route("/ws", get(ws_handler)) + // Canvas API + .route("/canvas/list", get(canvas_list)) + .route("/canvas/load", get(canvas_load)) + .route("/canvas/save", post(canvas_save)) + .route("/canvas/delete", delete(canvas_delete)) +} + +// ============ UDF Response Types ============ + +#[derive(Serialize)] +struct UdfConfig { + supported_resolutions: Vec<&'static str>, + supports_group_request: bool, + supports_marks: bool, + supports_search: bool, + supports_timescale_marks: bool, +} + +#[derive(Serialize)] +struct UdfSymbolInfo { + symbol: String, + ticker: String, + name: String, + full_name: String, + description: String, + exchange: String, + listed_exchange: String, + #[serde(rename = "type")] + symbol_type: String, + currency_code: String, + session: String, + timezone: String, + minmovement: i32, + minmov: i32, + minmovement2: i32, + minmov2: i32, + pricescale: i64, + supported_resolutions: Vec<&'static str>, + has_intraday: bool, + has_daily: bool, + has_weekly_and_monthly: bool, + data_status: String, +} + +#[derive(Serialize)] +struct UdfSearchResult { + symbol: String, + full_name: String, + description: String, + exchange: String, + ticker: String, + #[serde(rename = "type")] + symbol_type: String, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum UdfHistoryResponse { + Ok { + s: String, // "ok" + t: Vec, // timestamps (seconds) + o: Vec, // open + h: Vec, // high + l: Vec, // low + c: Vec, // close + v: Vec, // volume + nv: Vec, // net volume (custom) + tbv: Vec, // taker buy volume (custom) + }, + NoData { + s: String, // "no_data" + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "nextTime")] + next_time: Option, + }, + Error { + s: String, // "error" + errmsg: String, + }, +} + +// ============ Query Parameters ============ + +#[derive(Deserialize)] +struct SymbolQuery { + symbol: String, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +struct SearchQuery { + query: Option, + #[serde(rename = "type")] + symbol_type: Option, + exchange: Option, + limit: Option, +} + +#[derive(Deserialize)] +struct HistoryQuery { + symbol: String, + resolution: String, + from: i64, // unix timestamp (seconds) + to: i64, // unix timestamp (seconds) + countback: Option, +} + +// ============ Handlers ============ + +// GET /config +async fn get_config() -> Json { + Json(UdfConfig { + supported_resolutions: vec!["1", "5", "15", "60", "240", "1D", "1W", "1M"], + supports_group_request: false, + supports_marks: false, + supports_search: true, + supports_timescale_marks: false, + }) +} + +// GET /time +async fn get_time() -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + now.to_string() +} + +// GET /symbols?symbol=BTCUSDT +async fn get_symbol_info( + Query(query): Query, +) -> Json { + let symbol = query.symbol.to_uppercase(); + + Json(UdfSymbolInfo { + symbol: symbol.clone(), + ticker: symbol.clone(), + name: symbol.clone(), + full_name: format!("BINANCE:{}", symbol), + description: symbol.clone(), + exchange: "BINANCE".to_string(), + listed_exchange: "BINANCE".to_string(), + symbol_type: "crypto".to_string(), + currency_code: "USDT".to_string(), + session: "24x7".to_string(), + timezone: "Etc/UTC".to_string(), + minmovement: 1, + minmov: 1, + minmovement2: 0, + minmov2: 0, + pricescale: 100000000, // 8 decimal places for crypto + supported_resolutions: vec!["1", "5", "15", "60", "240", "1D", "1W", "1M"], + has_intraday: true, + has_daily: true, + has_weekly_and_monthly: true, + data_status: "streaming".to_string(), + }) +} + +// GET /tracked-symbols - 返回 TRACKED_SYMBOL 环境变量中配置的 symbols +async fn get_tracked_symbols() -> Json> { + Json(crate::DatabaseHandler::get_symbols_from_env()) +} + +// GET /daily-opens - 返回所有 symbol 当天 UTC 00:00 的开盘价 +async fn get_daily_opens( + State(state): State>, +) -> Json> { + match state.db.get_daily_opens().await { + Ok(opens) => Json(opens), + Err(_) => Json(std::collections::HashMap::new()), + } +} + +// GET /search?query=BTC&limit=10 +async fn search_symbols( + State(state): State>, + Query(query): Query, +) -> Json> { + let search_term = query.query.unwrap_or_default().to_uppercase(); + let limit = query.limit.unwrap_or(30) as usize; + + let symbols = match state.db.get_active_symbols().await { + Ok(s) => s, + Err(_) => return Json(vec![]), + }; + + let results: Vec = symbols + .into_iter() + .filter(|s| search_term.is_empty() || s.to_uppercase().contains(&search_term)) + .take(limit) + .map(|s| { + let upper = s.to_uppercase(); + UdfSearchResult { + symbol: upper.clone(), + full_name: format!("BINANCE:{}", upper), + description: upper.clone(), + exchange: "BINANCE".to_string(), + ticker: upper, + symbol_type: "crypto".to_string(), + } + }) + .collect(); + + Json(results) +} + +// GET /history?symbol=BTCUSDT&resolution=1&from=...&to=... +async fn get_history( + State(state): State>, + Query(query): Query, +) -> Json { + let symbol = query.symbol.to_uppercase(); + + // Convert resolution string to Interval enum + let interval = match query.resolution.as_str() { + "1" => Interval::Min1, + "5" => Interval::Min5, + "15" => Interval::Min15, + "60" => Interval::Hour1, + "240" => Interval::Hour4, + "D" | "1D" => Interval::Day1, + "W" | "1W" => Interval::Week1, + "M" | "1M" => Interval::Month1, + _ => Interval::Min1, + }; + + // Convert from/to (seconds) to milliseconds for database query + let from_ms = query.from * 1000; + let to_ms = query.to * 1000; + + // Calculate limit based on countback or time range + let limit = query.countback.unwrap_or(1000); + + match state.db.get_klines_aggregated(&symbol, interval, limit, Some(to_ms)).await { + Ok(candles) => { + // Filter by from_ms and convert to UDF format + let filtered: Vec<_> = candles + .into_iter() + .filter(|c| c.timestamp >= from_ms && c.timestamp <= to_ms) + .collect(); + + if filtered.is_empty() { + return Json(UdfHistoryResponse::NoData { + s: "no_data".to_string(), + next_time: None, + }); + } + + let t: Vec = filtered.iter().map(|c| c.timestamp / 1000).collect(); + let o: Vec = filtered.iter().map(|c| c.open).collect(); + let h: Vec = filtered.iter().map(|c| c.high).collect(); + let l: Vec = filtered.iter().map(|c| c.low).collect(); + let c: Vec = filtered.iter().map(|c| c.close).collect(); + let v: Vec = filtered.iter().map(|c| c.volume).collect(); + let nv: Vec = filtered.iter().map(|c| c.net_volume).collect(); + let tbv: Vec = filtered.iter().map(|c| c.taker_buy_volume).collect(); + + Json(UdfHistoryResponse::Ok { + s: "ok".to_string(), + t, o, h, l, c, v, nv, tbv, + }) + } + Err(e) => Json(UdfHistoryResponse::Error { + s: "error".to_string(), + errmsg: e.to_string(), + }), + } +} + +// ============ WebSocket Handler ============ + +async fn ws_handler( + ws: WebSocketUpgrade, + State(state): State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_ws_connection(socket, state)) +} + +async fn handle_ws_connection(socket: WebSocket, state: Arc) { + let (mut sender, mut receiver) = socket.split(); + + // Subscribe to broadcast channel + let mut candle_rx = state.candle_tx.subscribe(); + + // Subscribed symbols for this client + let subscribed_symbols: Arc>> = Arc::new(RwLock::new(HashSet::new())); + let subscribed_symbols_clone = subscribed_symbols.clone(); + + info!("WebSocket client connected"); + + // Task to receive messages from client + let recv_task = tokio::spawn(async move { + while let Some(msg) = receiver.next().await { + match msg { + Ok(Message::Text(text)) => { + // Parse incoming message + if let Ok(ws_msg) = serde_json::from_str::(&text) { + match ws_msg { + WsMessage::Subscribe { symbols } => { + let mut subs = subscribed_symbols_clone.write().await; + for s in symbols { + subs.insert(s.to_uppercase()); + } + debug!("Client subscribed to {} symbols", subs.len()); + } + WsMessage::Unsubscribe { symbols } => { + let mut subs = subscribed_symbols_clone.write().await; + for s in symbols { + subs.remove(&s.to_uppercase()); + } + } + WsMessage::Ping => { + // Pong is handled by the send task + } + _ => {} + } + } + } + Ok(Message::Close(_)) => { + info!("WebSocket client disconnected"); + break; + } + Err(e) => { + warn!("WebSocket receive error: {}", e); + break; + } + _ => {} + } + } + }); + + // Task to send messages to client + let send_task = tokio::spawn(async move { + loop { + tokio::select! { + // Forward candle updates to client + result = candle_rx.recv() => { + match result { + Ok(candle) => { + let subs = subscribed_symbols.read().await; + // Send to client if subscribed or if subscribed to all (empty set means all) + if subs.is_empty() || subs.contains(&candle.symbol.to_uppercase()) { + let msg = WsMessage::Kline(candle); + if let Ok(json) = serde_json::to_string(&msg) { + if sender.send(Message::Text(json.into())).await.is_err() { + break; + } + } + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!("WebSocket client lagged {} messages", n); + } + Err(broadcast::error::RecvError::Closed) => { + break; + } + } + } + } + } + }); + + // Wait for either task to finish + tokio::select! { + _ = recv_task => {} + _ = send_task => {} + } + + info!("WebSocket connection closed"); +} + +// ============ Canvas API ============ + +const STORAGE_DIR: &str = "storage"; +const DEFAULT_USER: &str = "default"; + +#[derive(Deserialize)] +struct CanvasListQuery { + symbol: String, +} + +#[derive(Deserialize)] +struct CanvasLoadQuery { + symbol: String, + name: String, +} + +#[derive(Deserialize)] +struct CanvasSaveBody { + symbol: String, + name: String, + data: serde_json::Value, +} + +#[derive(Deserialize)] +struct CanvasDeleteQuery { + symbol: String, + name: String, +} + +#[derive(Serialize)] +struct CanvasListResponse { + canvases: Vec, +} + +fn get_user_id(headers: &axum::http::HeaderMap) -> String { + headers.get("X-User-Id") + .and_then(|v| v.to_str().ok()) + .filter(|s| !s.is_empty()) + .unwrap_or(DEFAULT_USER) + .to_string() +} + +fn get_canvas_dir(user_id: &str, symbol: &str) -> PathBuf { + PathBuf::from(STORAGE_DIR) + .join(user_id) + .join(symbol.to_uppercase()) +} + +fn get_canvas_path(user_id: &str, symbol: &str, name: &str) -> PathBuf { + get_canvas_dir(user_id, symbol).join(format!("{}.json", name)) +} + +// GET /canvas/list?symbol=BTCUSDT +async fn canvas_list(headers: axum::http::HeaderMap, Query(query): Query) -> Json { + let user_id = get_user_id(&headers); + let dir = get_canvas_dir(&user_id, &query.symbol); + let mut canvases = Vec::new(); + + if let Ok(mut entries) = fs::read_dir(&dir).await { + while let Ok(Some(entry)) = entries.next_entry().await { + if let Some(name) = entry.file_name().to_str() { + if name.ends_with(".json") { + canvases.push(name.trim_end_matches(".json").to_string()); + } + } + } + } + + canvases.sort(); + Json(CanvasListResponse { canvases }) +} + +// GET /canvas/load?symbol=BTCUSDT&name=default +async fn canvas_load( + headers: axum::http::HeaderMap, + Query(query): Query, +) -> Result, StatusCode> { + let user_id = get_user_id(&headers); + let path = get_canvas_path(&user_id, &query.symbol, &query.name); + + match fs::read_to_string(&path).await { + Ok(content) => { + match serde_json::from_str(&content) { + Ok(data) => Ok(Json(data)), + Err(e) => { + error!("Failed to parse canvas {}: {}", path.display(), e); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } + } + Err(_) => Err(StatusCode::NOT_FOUND), + } +} + +// POST /canvas/save +async fn canvas_save( + headers: axum::http::HeaderMap, + Json(body): Json, +) -> Result { + let user_id = get_user_id(&headers); + let dir = get_canvas_dir(&user_id, &body.symbol); + let path = get_canvas_path(&user_id, &body.symbol, &body.name); + + // Create directory if not exists + if let Err(e) = fs::create_dir_all(&dir).await { + error!("Failed to create dir {}: {}", dir.display(), e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + + // Write canvas data + let content = serde_json::to_string_pretty(&body.data) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + fs::write(&path, content).await + .map_err(|e| { + error!("Failed to write canvas {}: {}", path.display(), e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + info!("Saved canvas: {}", path.display()); + Ok(StatusCode::OK) +} + +// DELETE /canvas/delete?symbol=BTCUSDT&name=default +async fn canvas_delete( + headers: axum::http::HeaderMap, + Query(query): Query, +) -> StatusCode { + let user_id = get_user_id(&headers); + let path = get_canvas_path(&user_id, &query.symbol, &query.name); + + match fs::remove_file(&path).await { + Ok(_) => { + info!("Deleted canvas: {}", path.display()); + StatusCode::OK + } + Err(_) => StatusCode::NOT_FOUND, + } +} diff --git a/backend/tests/connection_test.rs b/backend/tests/connection_test.rs new file mode 100644 index 0000000..8c19e4f --- /dev/null +++ b/backend/tests/connection_test.rs @@ -0,0 +1,86 @@ +use backend::{BinanceCollector, CandleData}; +use tokio::sync::mpsc; +use tokio::time::{timeout, Duration}; +use dotenv::dotenv; +use log::{info,error,debug}; + +#[tokio::test] +async fn test_backfill_and_stream() { + + dotenv().ok(); + env_logger::init(); + + let collector = BinanceCollector::new(vec!["btcusdt".to_string(), "ethusdt".to_string()]); + let (tx, mut rx) = mpsc::channel::(1000); + + // Calculate time range: last 10 minutes + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let start_time = now - (10 * 60 * 1000); // 10 minutes ago + + // First, backfill recent data for btcusdt + info!("Starting backfill..."); + let backfill_count = collector.backfill("btcusdt", start_time, now, &tx).await.unwrap(); + info!("Backfill completed: {} candles", backfill_count); + + // Then start WebSocket stream + let collector_handle = tokio::spawn(async move { + collector.start_stream(tx).await + }); + + let mut received_count = 0; + + // Drain backfill data first + while let Ok(Some(candle)) = timeout(Duration::from_millis(100), rx.recv()).await { + info!( + "[BACKFILL] {} ts={} c={:.2} net_vol={:.4}", + candle.symbol, candle.timestamp, candle.close, candle.net_volume + ); + received_count += 1; + } + info!("Received {} backfill candles from channel", received_count); + + // Wait for WebSocket data (up to 90 seconds for at least 1 closed candle) + let mut ws_count = 0; + let ws_result = timeout(Duration::from_secs(90), async { + while let Some(candle) = rx.recv().await { + info!( + "[WEBSOCKET] {} ts={} c={:.2} net_vol={:.4}", + candle.symbol, candle.timestamp, candle.close, candle.net_volume + ); + ws_count += 1; + if ws_count >= 1 { + return true; + } + } + false + }).await; + + collector_handle.abort(); + + info!("\nFinal: Backfill={}, WebSocket={}", backfill_count, ws_count); + + assert!(backfill_count >= 5, "Expected at least 5 backfill candles, got {}", backfill_count); + + match ws_result { + Ok(true) => info!("Test completed successfully!"), + Ok(false) => info!("WebSocket closed before receiving data"), + Err(_) => info!("WebSocket timed out (normal if test runs mid-minute)"), + } +} + +#[tokio::test] + +async fn test_get_symbol() { + + dotenv().ok(); + env_logger::init(); + + let collector = BinanceCollector::new(vec!["btcusdt".to_string(), "ethusdt".to_string()]); + + let result = BinanceCollector::get_symbol().await.unwrap(); + + info!("Result = {:?}",result); +} \ No newline at end of file diff --git a/backend/tests/database_test.rs b/backend/tests/database_test.rs new file mode 100644 index 0000000..bce2dad --- /dev/null +++ b/backend/tests/database_test.rs @@ -0,0 +1,128 @@ +use backend::{DatabaseHandler, CandleData}; +use tokio::sync::mpsc; +use dotenv::dotenv; +use log::info; +use std::env; + +fn get_database_url() -> String { + + dotenv().ok(); + env::var("DATABASE_URL").unwrap_or_else(|_| { + "postgres://quant:2Nr!Ya&oVvY5pp@172.18.0.10:5432/crypto_database".to_string() + }) +} + +#[tokio::test] +async fn test_database_connection() { + + dotenv().ok(); + env_logger::init(); + + let db_url = get_database_url(); + info!("Connecting to database..."); + + let db = DatabaseHandler::new(&db_url).await; + assert!(db.is_ok(), "Failed to connect to database: {:?}", db.err()); + + info!("Database connection successful!"); +} + +#[tokio::test] +async fn test_get_active_symbols() { + + dotenv().ok(); + env_logger::init(); + + let db_url = get_database_url(); + let db = DatabaseHandler::new(&db_url).await.expect("Failed to connect"); + + let symbols = db.get_active_symbols().await; + info!("Active symbols: {:?}", symbols); + + assert!(symbols.is_ok(), "Failed to get active symbols: {:?}", symbols.err()); +} + +#[tokio::test] +async fn test_insert_and_query() { + + dotenv().ok(); + env_logger::init(); + + let db_url = get_database_url(); + let db = DatabaseHandler::new(&db_url).await.expect("Failed to connect"); + + // Create test candle + let test_candle = CandleData { + symbol: "TESTUSDT".to_string(), + timestamp: 1700000000000, // Fixed test timestamp + open: 100.0, + high: 105.0, + low: 99.0, + close: 102.0, + volume: 1000.0, + taker_buy_volume: 600.0, + net_volume: 200.0, + is_closed: true, + }; + + // Insert + let result = db.insert_candle(&test_candle).await; + assert!(result.is_ok(), "Failed to insert candle: {:?}", result.err()); + info!("Inserted test candle"); + + // Query latest timestamp + let latest = db.get_latest_timestamp("TESTUSDT").await; + assert!(latest.is_ok(), "Failed to get latest timestamp: {:?}", latest.err()); + + let ts = latest.unwrap(); + assert!(ts.is_some(), "No timestamp found for TESTUSDT"); + assert_eq!(ts.unwrap(), 1700000000000, "Timestamp mismatch"); + + info!("Latest timestamp for TESTUSDT: {:?}", ts); +} + +#[tokio::test] +async fn test_batch_consumer() { + + dotenv().ok(); + env_logger::init(); + + let db_url = get_database_url(); + let db = DatabaseHandler::new(&db_url).await.expect("Failed to connect"); + + let (tx, rx) = mpsc::channel::(100); + + // Spawn consumer + let db_handle = tokio::spawn(async move { + db.start_consumer(rx).await; + }); + + // Send test candles + let base_ts = 1700000100000i64; + for i in 0..10 { + let candle = CandleData { + symbol: "BATCHTEST".to_string(), + timestamp: base_ts + (i * 60000), + open: 100.0 + i as f64, + high: 105.0 + i as f64, + low: 99.0 + i as f64, + close: 102.0 + i as f64, + volume: 1000.0, + taker_buy_volume: 600.0, + net_volume: 200.0, + is_closed: true, + }; + tx.send(candle).await.unwrap(); + } + + // Close channel to trigger flush + drop(tx); + + // Wait for consumer to finish + let _ = tokio::time::timeout( + tokio::time::Duration::from_secs(10), + db_handle + ).await; + + info!("Batch insert test completed"); +} diff --git a/backend/tests/scheduler_test.rs b/backend/tests/scheduler_test.rs new file mode 100644 index 0000000..fae5371 --- /dev/null +++ b/backend/tests/scheduler_test.rs @@ -0,0 +1,322 @@ +use backend::{ + Scheduler, DatabaseHandler, SchedulerCommand, + create_command_channel, +}; +use std::sync::Arc; +use tokio::sync::oneshot; +use tokio::time::{timeout, Duration}; +use dotenv::dotenv; +use log::info; +use std::env; + +fn get_database_url() -> String { + dotenv().ok(); + env::var("DATABASE_URL").unwrap_or_else(|_| { + "postgres://quant:2Nr!Ya&oVvY5pp@172.18.0.2:5432/crypto_database".to_string() + }) +} + +#[tokio::test] +async fn test_scheduler_startup_and_shutdown() { + + dotenv().ok(); + env_logger::try_init().ok(); + + let db_url = get_database_url(); + let db = Arc::new(DatabaseHandler::new(&db_url).await.expect("Failed to connect to database")); + + let (command_tx, command_rx) = create_command_channel(); + let mut scheduler = Scheduler::new(db.clone(), command_rx, None); + + // Start scheduler in background + let scheduler_handle = tokio::spawn(async move { + scheduler.run().await; + }); + + // Give it time to start + tokio::time::sleep(Duration::from_secs(2)).await; + + // Send shutdown command + command_tx.send(SchedulerCommand::Shutdown).await.expect("Failed to send shutdown"); + + // Wait for scheduler to stop + let result = timeout(Duration::from_secs(10), scheduler_handle).await; + assert!(result.is_ok(), "Scheduler did not shutdown in time"); + + info!("Scheduler startup and shutdown test passed"); +} + +#[tokio::test] +async fn test_get_status() { + + dotenv().ok(); + env_logger::try_init().ok(); + + let db_url = get_database_url(); + let db = Arc::new(DatabaseHandler::new(&db_url).await.expect("Failed to connect to database")); + + let (command_tx, command_rx) = create_command_channel(); + let mut scheduler = Scheduler::new(db.clone(), command_rx, None); + + // Start scheduler + let scheduler_handle = tokio::spawn(async move { + scheduler.run().await; + }); + + // Give it time to start + tokio::time::sleep(Duration::from_secs(2)).await; + + // Request status + let (reply_tx, reply_rx) = oneshot::channel(); + command_tx.send(SchedulerCommand::GetStatus { reply: reply_tx }).await.expect("Failed to send GetStatus"); + + let status = timeout(Duration::from_secs(5), reply_rx).await + .expect("Timeout waiting for status") + .expect("Failed to receive status"); + + info!("Scheduler status: {:?}", status); + assert!(status.is_running || status.active_symbols.is_empty(), "Scheduler should be running or have no symbols"); + + // Shutdown + command_tx.send(SchedulerCommand::Shutdown).await.expect("Failed to send shutdown"); + let _ = timeout(Duration::from_secs(10), scheduler_handle).await; + + info!("GetStatus test passed"); +} + +#[tokio::test] +async fn test_add_and_remove_symbol() { + + dotenv().ok(); + env_logger::try_init().ok(); + + let db_url = get_database_url(); + let db = Arc::new(DatabaseHandler::new(&db_url).await.expect("Failed to connect to database")); + + let test_symbol = "testscheduler"; + + // Clean up first - remove test symbol if exists + let _ = db.remove_symbol(test_symbol).await; + + let (command_tx, command_rx) = create_command_channel(); + let mut scheduler = Scheduler::new(db.clone(), command_rx, None); + + // Start scheduler + let scheduler_handle = tokio::spawn(async move { + scheduler.run().await; + }); + + // Give it time to start + tokio::time::sleep(Duration::from_secs(2)).await; + + // Add symbol (with recent backfill_from to avoid long backfill) + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let recent_time = now - (5 * 60 * 1000); // 5 minutes ago + + info!("Adding test symbol: {}", test_symbol); + command_tx.send(SchedulerCommand::AddSymbol { + symbol: test_symbol.to_string(), + backfill_from: Some(recent_time), + }).await.expect("Failed to send AddSymbol"); + + // Wait for processing + tokio::time::sleep(Duration::from_secs(5)).await; + + // Check status + let (reply_tx, reply_rx) = oneshot::channel(); + command_tx.send(SchedulerCommand::GetStatus { reply: reply_tx }).await.expect("Failed to send GetStatus"); + + let status = timeout(Duration::from_secs(5), reply_rx).await + .expect("Timeout") + .expect("Failed to receive"); + + info!("Status after add: {:?}", status); + + // Verify symbol was added + let is_tracked = db.is_symbol_tracked(test_symbol).await.expect("Failed to check tracking"); + assert!(is_tracked, "Symbol should be tracked after AddSymbol"); + + // Remove symbol + info!("Removing test symbol: {}", test_symbol); + command_tx.send(SchedulerCommand::RemoveSymbol { + symbol: test_symbol.to_string(), + }).await.expect("Failed to send RemoveSymbol"); + + // Wait for processing + tokio::time::sleep(Duration::from_secs(3)).await; + + // Verify symbol was removed + let is_tracked = db.is_symbol_tracked(test_symbol).await.expect("Failed to check tracking"); + assert!(!is_tracked, "Symbol should not be tracked after RemoveSymbol"); + + // Shutdown + command_tx.send(SchedulerCommand::Shutdown).await.expect("Failed to send shutdown"); + let _ = timeout(Duration::from_secs(10), scheduler_handle).await; + + info!("Add and remove symbol test passed"); +} + +#[tokio::test] +async fn test_restart_collector() { + + dotenv().ok(); + env_logger::try_init().ok(); + + let db_url = get_database_url(); + let db = Arc::new(DatabaseHandler::new(&db_url).await.expect("Failed to connect to database")); + + let (command_tx, command_rx) = create_command_channel(); + let mut scheduler = Scheduler::new(db.clone(), command_rx, None); + + // Start scheduler + let scheduler_handle = tokio::spawn(async move { + scheduler.run().await; + }); + + // Give it time to start + tokio::time::sleep(Duration::from_secs(2)).await; + + // Get initial status + let (reply_tx, reply_rx) = oneshot::channel(); + command_tx.send(SchedulerCommand::GetStatus { reply: reply_tx }).await.unwrap(); + let status_before = reply_rx.await.unwrap(); + info!("Status before restart: {:?}", status_before); + + // Send restart command + info!("Sending RestartCollector command"); + command_tx.send(SchedulerCommand::RestartCollector).await.expect("Failed to send RestartCollector"); + + // Wait for restart + tokio::time::sleep(Duration::from_secs(5)).await; + + // Get status after restart + let (reply_tx, reply_rx) = oneshot::channel(); + command_tx.send(SchedulerCommand::GetStatus { reply: reply_tx }).await.unwrap(); + let status_after = reply_rx.await.unwrap(); + info!("Status after restart: {:?}", status_after); + + // Shutdown + command_tx.send(SchedulerCommand::Shutdown).await.expect("Failed to send shutdown"); + let _ = timeout(Duration::from_secs(10), scheduler_handle).await; + + info!("Restart collector test passed"); +} + +#[tokio::test] +async fn test_multiple_commands() { + + dotenv().ok(); + env_logger::try_init().ok(); + + let db_url = get_database_url(); + let db = Arc::new(DatabaseHandler::new(&db_url).await.expect("Failed to connect to database")); + + let (command_tx, command_rx) = create_command_channel(); + let mut scheduler = Scheduler::new(db.clone(), command_rx, None); + + // Start scheduler + let scheduler_handle = tokio::spawn(async move { + scheduler.run().await; + }); + + tokio::time::sleep(Duration::from_secs(2)).await; + + // Send multiple status requests rapidly + for i in 0..5 { + let (reply_tx, reply_rx) = oneshot::channel(); + command_tx.send(SchedulerCommand::GetStatus { reply: reply_tx }).await.unwrap(); + let status = reply_rx.await.unwrap(); + info!("Status request {}: {:?}", i, status); + } + + // Shutdown + command_tx.send(SchedulerCommand::Shutdown).await.expect("Failed to send shutdown"); + let _ = timeout(Duration::from_secs(10), scheduler_handle).await; + + info!("Multiple commands test passed"); +} + +#[tokio::test] +async fn test_real_btcusdt_collection() { + + dotenv().ok(); + env_logger::try_init().ok(); + + let db_url = get_database_url(); + let db = Arc::new(DatabaseHandler::new(&db_url).await.expect("Failed to connect to database")); + + let symbol = "btcusdt"; + + // Clean up - remove from tracking first + let _ = db.remove_symbol(symbol).await; + + let (command_tx, command_rx) = create_command_channel(); + let mut scheduler = Scheduler::new(db.clone(), command_rx, None); + + // Start scheduler + let scheduler_handle = tokio::spawn(async move { + scheduler.run().await; + }); + + tokio::time::sleep(Duration::from_secs(2)).await; + + // Calculate start time: 5 minutes ago + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let five_min_ago = now - (5 * 60 * 1000); + + info!("Adding BTCUSDT, backfill from {} (5 min ago)", five_min_ago); + + // Add BTCUSDT with backfill from 5 minutes ago + command_tx.send(SchedulerCommand::AddSymbol { + symbol: symbol.to_string(), + backfill_from: Some(five_min_ago), + }).await.expect("Failed to send AddSymbol"); + + // Wait for backfill to complete + info!("Waiting for backfill..."); + tokio::time::sleep(Duration::from_secs(10)).await; + + // Check latest timestamp in database + let latest_ts = db.get_latest_timestamp(symbol).await.expect("Failed to get timestamp"); + info!("Latest BTCUSDT timestamp in DB: {:?}", latest_ts); + + assert!(latest_ts.is_some(), "Should have BTCUSDT data in database"); + + // Wait for 2 more candles (about 2 minutes + buffer) + info!("Waiting for 2 live candles (~2.5 minutes)..."); + tokio::time::sleep(Duration::from_secs(150)).await; + + // Check new latest timestamp + let new_latest_ts = db.get_latest_timestamp(symbol).await.expect("Failed to get timestamp"); + info!("New latest BTCUSDT timestamp: {:?}", new_latest_ts); + + assert!(new_latest_ts.unwrap() > latest_ts.unwrap(), "Should have received new candles"); + + // Get status + let (reply_tx, reply_rx) = oneshot::channel(); + command_tx.send(SchedulerCommand::GetStatus { reply: reply_tx }).await.unwrap(); + let status = reply_rx.await.unwrap(); + info!("Final status: {:?}", status); + + assert!(status.active_symbols.contains(&symbol.to_string()), "BTCUSDT should be in active symbols"); + + // Clean up - remove symbol + command_tx.send(SchedulerCommand::RemoveSymbol { + symbol: symbol.to_string(), + }).await.expect("Failed to remove symbol"); + + tokio::time::sleep(Duration::from_secs(2)).await; + + // Shutdown + command_tx.send(SchedulerCommand::Shutdown).await.expect("Failed to send shutdown"); + let _ = timeout(Duration::from_secs(10), scheduler_handle).await; + + info!("Real BTCUSDT collection test passed!"); + info!("Check database: SELECT * FROM klines_1m WHERE symbol = 'BTCUSDT' ORDER BY timestamp DESC LIMIT 10;"); +} diff --git a/backend/tests/sync_full_history_test.rs b/backend/tests/sync_full_history_test.rs new file mode 100644 index 0000000..89afa1b --- /dev/null +++ b/backend/tests/sync_full_history_test.rs @@ -0,0 +1,65 @@ +use backend::{BinanceCollector, CandleData}; +use tokio::sync::mpsc; +use dotenv::dotenv; +use log::info; + +#[tokio::test] +async fn test_sync_full_history_single_symbol() { + dotenv().ok(); + env_logger::init(); + + // 1. Get all symbols + let symbol_vec = BinanceCollector::get_symbol().await.unwrap(); + info!("Got {} symbols", symbol_vec.len()); + + // 2. Find ETHUSDT start time + let test_symbol = "ETHUSDT"; + let test_start_time = symbol_vec + .iter() + .find(|item| item.symbol == test_symbol) + .map(|item| item.start_timestamp) + .expect("ETHUSDT not found"); + + info!("{} start_timestamp: {}", test_symbol, test_start_time); + + // 3. Build clients (all clients work together on this symbol) + let clients = BinanceCollector::build_clients().await; + info!("Built {} clients", clients.len()); + assert!(!clients.is_empty(), "Need at least 1 client"); + + // 4. Create channel + let (tx, mut rx) = mpsc::channel::(100000); + + // 5. Sync from scratch (all clients work in parallel on different time segments) + info!("Starting full history sync for {} with {} clients...", test_symbol, clients.len()); + + let sync_handle = tokio::spawn(async move { + BinanceCollector::sync_from_scratch( + test_symbol.to_string(), + test_start_time, + clients, // Pass all clients + tx, + ).await + }); + + // 6. Consume and count + let mut count = 0u64; + let mut last_ts = 0i64; + + while let Some(candle) = rx.recv().await { + count += 1; + last_ts = candle.timestamp; + + if count % 100000 == 0 { + info!("Progress: {} candles, last_ts: {}", count, last_ts); + } + } + + let result = sync_handle.await.unwrap(); + + info!("Sync result: {:?}", result); + info!("Total received: {} candles", count); + info!("Last timestamp: {}", last_ts); + + assert!(count > 0, "Should have synced some candles"); +} diff --git a/backend/tests/sync_test.rs b/backend/tests/sync_test.rs new file mode 100644 index 0000000..5734461 --- /dev/null +++ b/backend/tests/sync_test.rs @@ -0,0 +1,68 @@ +use backend::{BinanceCollector, CandleData}; +use tokio::sync::mpsc; +use dotenv::dotenv; +use log::info; + +#[tokio::test] +async fn test_build_clients() { + dotenv().ok(); + env_logger::init(); + + info!("Testing build_clients..."); + + let clients = BinanceCollector::build_clients().await; + + info!("Built {} working clients", clients.len()); + assert!(clients.len() > 0, "Should have at least 1 working client"); +} + +#[tokio::test] +async fn test_sync_single_symbol() { + dotenv().ok(); + let _ = env_logger::try_init(); + + info!("Testing sync_from_scratch for single symbol..."); + + // Build clients (all work together) + let clients = BinanceCollector::build_clients().await; + assert!(!clients.is_empty(), "Need at least 1 client"); + info!("Built {} clients", clients.len()); + + // Create channel + let (tx, mut rx) = mpsc::channel::(10000); + + // Sync last 5 minutes of BTCUSDT + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let start_time = now - (5 * 60 * 1000); // 5 minutes ago + + info!("Syncing BTCUSDT from {} to {}", start_time, now); + + let count = BinanceCollector::sync_from_scratch( + "BTCUSDT".to_string(), + start_time, + clients, // All clients work together + tx, + ).await.unwrap(); + + info!("Sync returned {} candles", count); + + // Drain channel + let mut received = 0; + while let Ok(candle) = rx.try_recv() { + info!( + "Candle: {} ts={} o={:.2} h={:.2} l={:.2} c={:.2} nv={:.4}", + candle.symbol, candle.timestamp, + candle.open, candle.high, candle.low, candle.close, + candle.net_volume + ); + received += 1; + } + + info!("Received {} candles from channel", received); + assert!(count >= 3, "Expected at least 3 candles for 5 min, got {}", count); +} + + diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..013f1ac --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +API_BASE_URL=https://api.yourdomain.com \ No newline at end of file diff --git a/frontend/.github/ISSUE_TEMPLATE/Bug_report.md b/frontend/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000..2c0b534 --- /dev/null +++ b/frontend/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,59 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +# Bug Report + +**Charting Library Version** + +Execute `TradingView.version()` in the browser console. + +**Desktop (please complete the following information):** + + - OS: [e.g. Windows 11] + - Browser [e.g. chrome, safari] + - Browser version [e.g. 22] + +**Smartphone (please complete the following information):** + + - Device: [e.g. iPhone 13] + - OS: [e.g. iOS 14.1] + - Browser [e.g. stock browser, safari] + - Browser version [e.g. 22] + +**JSFiddle** + +If possible, please provide an example in JSFiddle which demonstrates the bug. Please see this guide on how to create a JSFiddle: [Using the library with online code playgrounds](https://github.com/tradingview/charting_library/wiki/Online-Editors) + +**To Reproduce** + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Describe the bug** + +A clear and concise description of what the bug is. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots, or videos, to help explain your problem. + +**Debug Console Log** + +Please provide the entire console log produced when [`debug:true`](https://github.com/tradingview/charting_library/wiki/Widget-Constructor#debug) is set within the widget constructor options, or evoke the following command in the console: [`widget.setDebugMode(true)`](https://github.com/tradingview/charting_library/wiki/Widget-Methods#setdebugmodeenabled). + +**Additional context** + +Add any other context about the problem here. diff --git a/frontend/.github/ISSUE_TEMPLATE/Data_question.md b/frontend/.github/ISSUE_TEMPLATE/Data_question.md new file mode 100644 index 0000000..74517dd --- /dev/null +++ b/frontend/.github/ISSUE_TEMPLATE/Data_question.md @@ -0,0 +1,27 @@ +--- +name: Data Question +about: Ask a question if you have a problem with connecting your data +title: '' +labels: status/conversation +assignees: '' + +--- + +# Data Question + + + + +- [ ] I have read FAQ + +**Describe the problem** + +A clear and concise description of what the problem is. + +**Console logs** + +Uncomment `debug: true` in the widget constructor and post here all console logs. ```Use a triple backtick in front and behind the logs.``` + +**Symbol** + +What symbol(instrument) your problem is related to. diff --git a/frontend/.github/ISSUE_TEMPLATE/Feature_request.md b/frontend/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..e28e2e8 --- /dev/null +++ b/frontend/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +# Feature Request + +**Is your feature request related to a problem? Please describe.** + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** + +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** + +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** + +Add any other context or screenshots about the feature request here. diff --git a/frontend/.github/ISSUE_TEMPLATE/General_question.md b/frontend/.github/ISSUE_TEMPLATE/General_question.md new file mode 100644 index 0000000..a9f7e36 --- /dev/null +++ b/frontend/.github/ISSUE_TEMPLATE/General_question.md @@ -0,0 +1,19 @@ +--- +name: General Question +about: Ask a question if you cannot find an answer in the documentation +title: '' +labels: status/conversation +assignees: '' + +--- + +# General Question + + + + +- [ ] I have read FAQ + +**Ask your question with the greatest possible detail** + + diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..cf2d769 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1 @@ +img \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..2c339f4 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine +COPY . /usr/share/nginx/html/ +EXPOSE 80 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..93c549b --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,143 @@ +# 📈 View Charting Frontend + +A self-hosted [View Charting Library](https://www.tradingview.com/charting-library-docs/) frontend for Binance USDS-M Futures. Features real-time WebSocket streaming, multi-chart layouts, persistent drawing canvases, and a watchlist sidebar — all protected behind Google OAuth. + +## ✨ Features + +- **View Charting Library v29.4** — Professional-grade charting with full indicator and drawing tool support +- **Real-time Data** — Live candle updates via WebSocket from the backend API +- **Multi-chart Layouts** — Single, vertical split, horizontal split, and 1-left / 2-right layouts with resizable dividers +- **Watchlist Sidebar** — Drag-and-drop reordering, live price & change tracking, multiple custom lists +- **Canvas Persistence** — Save/load named drawing canvases per symbol, with auto-save every 5 minutes +- **Dark & Light Theme** — Full theme support synced with View widget +- **Google OAuth Protection** — Access restricted to authorized Google accounts with 144-hour session persistence +- **Responsive Login Page** — Adaptive background images for mobile / tablet / desktop +- **Docker Ready** — Nginx-based container, single-file deployment + +## 📁 Project Structure + +``` +├── index.html # Main application (View widget + watchlist + layouts) +├── login.html # Standalone login page with Google Sign-In +├── auth.js # AuthGuard class — session management & login UI +├── auth-config.js # Google OAuth Client ID & API base URL configuration +├── .env.example # Environment variable template +├── Dockerfile # Nginx Alpine container +├── docker-compose.yml # Docker Compose service definition +├── serve.py # Python dev server with CORS support +├── package.json # Charting Library package metadata (v29.4.0) +├── charting_library/ # View Charting Library assets +└── datafeeds/ # View UDF datafeed adapter +``` + +## 🚀 Quick Start + +### Prerequisites + +- A running [backend service](../backend/) providing the API +- (Optional) Google OAuth Client ID from [GCP Console](https://console.cloud.google.com/apis/credentials) for authentication + +### Environment Setup + +```bash +cp .env.example .env +# Edit .env with your backend API URL +``` + +`.env.example`: +```env +API_BASE_URL=https://api.yourdomain.com +``` + +### Configuration + +Edit `auth-config.js` to set your API base URL and Google OAuth Client ID: + +```js +window.API_CONFIG = { baseUrl: 'https://api.yourdomain.com' }; + +const AUTH_CONFIG = { + clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com', + onSuccess: (user) => { /* ... */ }, + onError: (error) => { /* ... */ } +}; +``` + +### Development + +```bash +# Using Python dev server (with CORS headers) +python3 serve.py +# → http://localhost:8080 + +# Or any static file server +npx serve . +``` + +### Docker + +```bash +# Build image +docker build -t frontend . + +# Using docker-compose (connects to existing `cycle` network) +docker compose up -d +``` + +## 🔐 Authentication Flow + +``` +login.html index.html + │ │ + ├─ Google Sign-In ──────┐ │ + │ ▼ │ + │ JWT validation │ + │ + email whitelist │ + │ │ + │ localStorage ───────┤ + │ (auth_user, │ + │ auth_token, │ + │ auth_login_time) │ + │ │ + │ redirect ───────────▶ Session check (144h max) │ + │ │ + │ ├─ Valid → Load app │ + │ └─ Expired → Redirect to login.html +``` + +### Email Whitelist + +Access is restricted at the client level via an email whitelist in `login.html`. Only emails in the `allowedEmails` array can log in — all others receive an "Access denied" error: + +```js +const allowedEmails = ['your-email@gmail.com']; +``` + +> [!IMPORTANT] +> Update this whitelist with your authorized email addresses before deploying. This is a **client-side** check; for production use, combine with GCP Console's OAuth test user list for server-side enforcement. + +## 🖥️ Multi-Chart Layouts + +| Layout | Description | +|--------|-------------| +| **Single** | One full-screen chart | +| **Vertical** | Two charts stacked vertically | +| **Horizontal** | Two charts side by side | +| **1L + 2R** | One large chart on left, two stacked on right | + +All dividers are draggable for custom sizing. + +## 📦 Dependencies + +| Dependency | Purpose | +|------------|---------| +| [View Charting Library](https://www.tradingview.com/charting-library-docs/) v29.4 | Charting engine | +| [Google Identity Services](https://developers.google.com/identity/gsi/web) | OAuth authentication | +| Nginx Alpine | Production static file serving | + +## 📜 License + +MIT + + +Generated By Claude Opus 4.6 diff --git a/frontend/auth-config.js b/frontend/auth-config.js new file mode 100644 index 0000000..9b496c7 --- /dev/null +++ b/frontend/auth-config.js @@ -0,0 +1,28 @@ +window.API_CONFIG = { baseUrl: 'https://xxx.xxx.com' }; + +/** + * Google OAuth Configuration + * Configure your Google OAuth Client ID here + * Access control is managed via the test users list in GCP Console + */ + +const AUTH_CONFIG = { + // Client ID obtained from GCP Console + // Replace with your actual Client ID + clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com', + + // Authentication success callback + onSuccess: (user) => { + console.log('Authentication successful:', user.email); + // Additional logic can be added here, e.g. logging + }, + + // Authentication failure callback + onError: (error) => { + console.error('Authentication error:', error); + // Error handling logic can be added here + } +}; + +// Export configuration +window.AUTH_CONFIG = AUTH_CONFIG; diff --git a/frontend/auth.js b/frontend/auth.js new file mode 100644 index 0000000..e839f05 --- /dev/null +++ b/frontend/auth.js @@ -0,0 +1,278 @@ +/** + * Google OAuth Login Guard + * Only authorized Google accounts can access the application + */ + +class AuthGuard { + constructor(config) { + this.clientId = config.clientId; + this.onSuccess = config.onSuccess || (() => { }); + this.onError = config.onError || ((error) => console.error(error)); + + this.user = null; + this.isAuthenticated = false; + } + + /** + * Initialize Google Sign-In + */ + init() { + return new Promise((resolve, reject) => { + // Load Google Identity Services + const script = document.createElement('script'); + script.src = 'https://accounts.google.com/gsi/client'; + script.async = true; + script.defer = true; + + script.onload = () => { + this.initializeGoogleAuth(); + resolve(); + }; + + script.onerror = () => { + reject(new Error('Failed to load Google Identity Services')); + }; + + document.head.appendChild(script); + }); + } + + /** + * Initialize Google OAuth + */ + initializeGoogleAuth() { + google.accounts.id.initialize({ + client_id: this.clientId, + callback: this.handleCredentialResponse.bind(this), + auto_select: true, + cancel_on_tap_outside: false + }); + + // Check if already logged in + this.checkExistingSession(); + } + + /** + * Check locally stored session + */ + checkExistingSession() { + const savedUser = localStorage.getItem('auth_user'); + const savedToken = localStorage.getItem('auth_token'); + + if (savedUser && savedToken) { + try { + const user = JSON.parse(savedUser); + // Verify if token is expired + const tokenData = this.parseJwt(savedToken); + if (tokenData.exp * 1000 > Date.now()) { + this.user = user; + this.isAuthenticated = true; + this.onSuccess(user); + this.hideLoginUI(); + return; + } + } catch (e) { + console.error('Invalid session data:', e); + } + } + + // No valid session, show login UI + this.showLoginUI(); + } + + /** + * Handle Google login response + */ + handleCredentialResponse(response) { + try { + const credential = response.credential; + const userData = this.parseJwt(credential); + + // Save user info and token + this.user = { + email: userData.email, + name: userData.name, + picture: userData.picture, + sub: userData.sub + }; + this.isAuthenticated = true; + + localStorage.setItem('auth_user', JSON.stringify(this.user)); + localStorage.setItem('auth_token', credential); + + this.hideLoginUI(); + this.onSuccess(this.user); + + // Reload page to initialize the application + location.reload(); + + } catch (error) { + this.onError({ + type: 'AUTH_ERROR', + message: 'Authentication failed', + error + }); + } + } + + /** + * Show login UI + */ + showLoginUI() { + const loginContainer = document.createElement('div'); + loginContainer.id = 'auth-container'; + loginContainer.innerHTML = ` + +
+ +
+ + +
+

Tradingview

+

Sign in with your authorized Google account to continue

+
+
+
+ `; + + document.body.appendChild(loginContainer); + + // Render Google Sign-In button + google.accounts.id.renderButton( + document.getElementById('google-signin-button'), + { + theme: 'filled_blue', + size: 'large', + text: 'signin_with', + shape: 'rectangular', + width: 300 + } + ); + } + + + /** + * Hide login UI + */ + hideLoginUI() { + const container = document.getElementById('auth-container'); + if (container) { + container.remove(); + } + } + + /** + * Logout + */ + logout() { + this.user = null; + this.isAuthenticated = false; + localStorage.removeItem('auth_user'); + localStorage.removeItem('auth_token'); + + // Clear Google session + google.accounts.id.disableAutoSelect(); + + // Reload page + location.reload(); + } + + /** + * Parse JWT token + */ + parseJwt(token) { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + return JSON.parse(jsonPayload); + } + + /** + * Get current user + */ + getUser() { + return this.user; + } + + /** + * Check if user is authenticated + */ + isUserAuthenticated() { + return this.isAuthenticated; + } +} + +// Export to global scope +window.AuthGuard = AuthGuard; diff --git a/frontend/charting_library.d.ts b/frontend/charting_library.d.ts new file mode 100644 index 0000000..250a5c6 --- /dev/null +++ b/frontend/charting_library.d.ts @@ -0,0 +1,5 @@ +export * from './charting_library/charting_library'; +declare module 'charting_library/datafeeds/udf/dist/bundle' { + const UDFCompatibleDatafeed: typeof import('./datafeeds/udf/src/udf-compatible-datafeed').UDFCompatibleDatafeed; + export { UDFCompatibleDatafeed }; +} diff --git a/frontend/charting_library/bundles/1072.67a2846c0506e2e592be.css b/frontend/charting_library/bundles/1072.67a2846c0506e2e592be.css new file mode 100644 index 0000000..0421f72 --- /dev/null +++ b/frontend/charting_library/bundles/1072.67a2846c0506e2e592be.css @@ -0,0 +1 @@ +.container-qm7Rg5MB{align-items:center;border-bottom:1px solid;border-top:1px solid;border-color:var(--themed-color-search-border,#ebebeb);cursor:default;display:flex;flex-shrink:0;position:relative}html.theme-dark .container-qm7Rg5MB{border-color:var(--themed-color-search-border,#4a4a4a)}.container-qm7Rg5MB.mobile-qm7Rg5MB{background-color:var(--themed-color-container-fill-primary-neutral-extra-light,#f2f2f2);border:none;border-radius:40px;margin:4px 20px 8px}html.theme-dark .container-qm7Rg5MB.mobile-qm7Rg5MB{background-color:var(--themed-color-container-fill-primary-neutral-extra-light,#303030)}.inputContainer-qm7Rg5MB{height:24px;padding:8px 16px 8px 47px;width:100%}.inputContainer-qm7Rg5MB.mobile-qm7Rg5MB{padding-left:40px}.inputContainer-qm7Rg5MB.withCancel-qm7Rg5MB{padding-right:70px}.input-qm7Rg5MB{background-color:initial;border:none;color:var(--themed-color-load-chart-dialog-text,#1a1a1a);font-size:16px;height:100%;margin:0;padding:0;width:100%}html.theme-dark .input-qm7Rg5MB{color:var(--themed-color-load-chart-dialog-text,#a8a8a8)}.input-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral-bold,#1a1a1a)}html.theme-dark .input-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral-bold,#dbdbdb)}.input-qm7Rg5MB.mobile-qm7Rg5MB::placeholder{color:var(--themed-color-content-primary-neutral,#707070);font-weight:400}html.theme-dark .input-qm7Rg5MB.mobile-qm7Rg5MB::placeholder{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.input-qm7Rg5MB::placeholder{color:var(--themed-color-input-placeholder-text,#a8a8a8);font-weight:400}html.theme-dark .input-qm7Rg5MB::placeholder{color:var(--themed-color-input-placeholder-text,#4a4a4a)}.icon-qm7Rg5MB{color:#a8a8a8;height:28px;left:15px;pointer-events:none;position:absolute;top:calc(50% - 14px)}.icon-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#707070);left:8px}html.theme-dark .icon-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.cancel-qm7Rg5MB{color:var(--themed-color-default-gray,#707070);position:absolute;right:20px}html.theme-dark .cancel-qm7Rg5MB{color:var(--themed-color-default-gray,#8c8c8c)}.cancel-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#707070)}html.theme-dark .cancel-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.highlighted-cwp8YRo6,html.theme-dark .highlighted-cwp8YRo6{color:var(--themed-color-brand,#2962ff)} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1072.67a2846c0506e2e592be.rtl.css b/frontend/charting_library/bundles/1072.67a2846c0506e2e592be.rtl.css new file mode 100644 index 0000000..34f4620 --- /dev/null +++ b/frontend/charting_library/bundles/1072.67a2846c0506e2e592be.rtl.css @@ -0,0 +1 @@ +.container-qm7Rg5MB{align-items:center;border-bottom:1px solid;border-top:1px solid;border-color:var(--themed-color-search-border,#ebebeb);cursor:default;display:flex;flex-shrink:0;position:relative}html.theme-dark .container-qm7Rg5MB{border-color:var(--themed-color-search-border,#4a4a4a)}.container-qm7Rg5MB.mobile-qm7Rg5MB{background-color:var(--themed-color-container-fill-primary-neutral-extra-light,#f2f2f2);border:none;border-radius:40px;margin:4px 20px 8px}html.theme-dark .container-qm7Rg5MB.mobile-qm7Rg5MB{background-color:var(--themed-color-container-fill-primary-neutral-extra-light,#303030)}.inputContainer-qm7Rg5MB{height:24px;padding:8px 47px 8px 16px;width:100%}.inputContainer-qm7Rg5MB.mobile-qm7Rg5MB{padding-right:40px}.inputContainer-qm7Rg5MB.withCancel-qm7Rg5MB{padding-left:70px}.input-qm7Rg5MB{background-color:initial;border:none;color:var(--themed-color-load-chart-dialog-text,#1a1a1a);font-size:16px;height:100%;margin:0;padding:0;width:100%}html.theme-dark .input-qm7Rg5MB{color:var(--themed-color-load-chart-dialog-text,#a8a8a8)}.input-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral-bold,#1a1a1a)}html.theme-dark .input-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral-bold,#dbdbdb)}.input-qm7Rg5MB.mobile-qm7Rg5MB::placeholder{color:var(--themed-color-content-primary-neutral,#707070);font-weight:400}html.theme-dark .input-qm7Rg5MB.mobile-qm7Rg5MB::placeholder{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.input-qm7Rg5MB::placeholder{color:var(--themed-color-input-placeholder-text,#a8a8a8);font-weight:400}html.theme-dark .input-qm7Rg5MB::placeholder{color:var(--themed-color-input-placeholder-text,#4a4a4a)}.icon-qm7Rg5MB{color:#a8a8a8;height:28px;pointer-events:none;position:absolute;right:15px;top:calc(50% - 14px)}.icon-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#707070);right:8px}html.theme-dark .icon-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.cancel-qm7Rg5MB{color:var(--themed-color-default-gray,#707070);left:20px;position:absolute}html.theme-dark .cancel-qm7Rg5MB{color:var(--themed-color-default-gray,#8c8c8c)}.cancel-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#707070)}html.theme-dark .cancel-qm7Rg5MB.mobile-qm7Rg5MB{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.highlighted-cwp8YRo6,html.theme-dark .highlighted-cwp8YRo6{color:var(--themed-color-brand,#2962ff)} \ No newline at end of file diff --git a/frontend/charting_library/bundles/116.668ae3395c34e5ab58d7.css b/frontend/charting_library/bundles/116.668ae3395c34e5ab58d7.css new file mode 100644 index 0000000..7aaaf06 --- /dev/null +++ b/frontend/charting_library/bundles/116.668ae3395c34e5ab58d7.css @@ -0,0 +1 @@ +.hidden-DgcIT6Uz{opacity:0}.fadeInWrapper-DgcIT6Uz{transition:opacity var(--ui-lib-fadeInWrapper-transition-duration,.35s);width:100%}.roundTabButton-JbssaNvk{align-items:center;border-style:solid;border-width:1px;box-sizing:border-box;cursor:default;display:inline-flex;flex:0 0 auto;justify-content:center;max-width:100%;-webkit-tap-highlight-color:transparent;outline:none;overflow:visible;position:relative;-webkit-user-select:none;user-select:none}.roundTabButton-JbssaNvk:focus{outline:none}.roundTabButton-JbssaNvk:focus-visible{outline:none}.roundTabButton-JbssaNvk:after{border-style:solid;border-width:2px;box-sizing:border-box;content:"";display:none;height:calc(100% + 10px);left:-5px;pointer-events:none;position:absolute;top:-5px;width:calc(100% + 10px);z-index:1}.roundTabButton-JbssaNvk:focus:after{display:block}.roundTabButton-JbssaNvk:focus-visible:after{display:block}.roundTabButton-JbssaNvk:focus:not(:focus-visible):after{display:none}.roundTabButton-JbssaNvk:after,html.theme-dark .roundTabButton-JbssaNvk:after{border-color:var(--themed-color-focus-outline-color-blue,#2962ff)}.roundTabButton-JbssaNvk.disableFocusOutline-JbssaNvk:after{display:none}.roundTabButton-JbssaNvk.enableCursorPointer-JbssaNvk{cursor:pointer}.roundTabButton-JbssaNvk:not(:first-child){margin-inline-start:var(--ui-lib-round-tab-margin)}.roundTabButton-JbssaNvk.large-JbssaNvk{border-radius:24px;height:48px;padding:0 23px}.roundTabButton-JbssaNvk.large-JbssaNvk:after{border-radius:28px}.roundTabButton-JbssaNvk.large-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.large-JbssaNvk.withStartIcon-JbssaNvk{padding-inline-start:15px}.roundTabButton-JbssaNvk.large-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.large-JbssaNvk.withEndIcon-JbssaNvk{padding-inline-end:15px}.roundTabButton-JbssaNvk.large-JbssaNvk:not(.iconOnly-JbssaNvk) .startIconWrap-JbssaNvk{margin-inline-end:4px}.roundTabButton-JbssaNvk.large-JbssaNvk .endIconWrap-JbssaNvk{margin-inline-start:4px}.roundTabButton-JbssaNvk.small-JbssaNvk{border-radius:17px;height:34px;padding:0 15px}.roundTabButton-JbssaNvk.small-JbssaNvk:after{border-radius:21px}.roundTabButton-JbssaNvk.small-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.small-JbssaNvk.withStartIcon-JbssaNvk{padding-inline-start:11px}.roundTabButton-JbssaNvk.small-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.small-JbssaNvk.withEndIcon-JbssaNvk{padding-inline-end:11px}.roundTabButton-JbssaNvk.small-JbssaNvk:not(.iconOnly-JbssaNvk) .startIconWrap-JbssaNvk{margin-inline-end:4px}.roundTabButton-JbssaNvk.small-JbssaNvk .endIconWrap-JbssaNvk{margin-inline-start:4px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk{border-radius:14px;height:28px;padding:0 11px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk:after{border-radius:18px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.xsmall-JbssaNvk.withStartIcon-JbssaNvk{padding-inline-start:7px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.xsmall-JbssaNvk.withEndIcon-JbssaNvk{padding-inline-end:7px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk:not(.iconOnly-JbssaNvk) .startIconWrap-JbssaNvk{margin-inline-end:4px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk .endIconWrap-JbssaNvk{margin-inline-start:2px}.roundTabButton-JbssaNvk.large-JbssaNvk{font-feature-settings:"tnum" on,"lnum" on;font-size:var(--ui-lib-typography-font-size);font-style:normal;font-weight:500}.roundTabButton-JbssaNvk.large-JbssaNvk,.roundTabButton-JbssaNvk.small-JbssaNvk{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;--ui-lib-typography-font-size:16px;--ui-lib-typography-line-height:24px;line-height:var(--ui-lib-typography-line-height)}.roundTabButton-JbssaNvk.small-JbssaNvk{font-feature-settings:"tnum" on,"lnum" on;font-size:var(--ui-lib-typography-font-size);font-style:normal;font-weight:400}.roundTabButton-JbssaNvk.xsmall-JbssaNvk{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:14px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:18px;line-height:var(--ui-lib-typography-line-height)}.roundTabButton-JbssaNvk.primary-JbssaNvk{background-color:var(--themed-color-round-tab-primary-default-bg,#f2f2f2);border-color:var(--themed-color-round-tab-primary-default-border,#f2f2f2);color:var(--themed-color-round-tab-primary-default-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk{background-color:var(--themed-color-round-tab-primary-default-bg,#303030);border-color:var(--themed-color-round-tab-primary-default-border,#303030);color:var(--themed-color-round-tab-primary-default-content,#dbdbdb)}@media (any-hover:hover){.roundTabButton-JbssaNvk.primary-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-primary-hover-bg,#ebebeb);border-color:var(--themed-color-round-tab-primary-hover-border,#ebebeb);color:var(--themed-color-round-tab-primary-hover-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-primary-hover-bg,#3d3d3d);border-color:var(--themed-color-round-tab-primary-hover-border,#3d3d3d);color:var(--themed-color-round-tab-primary-hover-content,#dbdbdb)}}.roundTabButton-JbssaNvk.primary-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-primary-active-bg,#dbdbdb);border-color:var(--themed-color-round-tab-primary-active-border,#dbdbdb);color:var(--themed-color-round-tab-primary-active-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-primary-active-bg,#4a4a4a);border-color:var(--themed-color-round-tab-primary-active-border,#4a4a4a);color:var(--themed-color-round-tab-primary-active-content,#fff)}.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk,.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-primary-selected-bg,#1a1a1a);border-color:var(--themed-color-round-tab-primary-selected-border,#1a1a1a);color:var(--themed-color-round-tab-primary-selected-content,#fff)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-primary-selected-bg,#fff);border-color:var(--themed-color-round-tab-primary-selected-border,#fff);color:var(--themed-color-round-tab-primary-selected-content,#1a1a1a)}.roundTabButton-JbssaNvk.ghost-JbssaNvk{background-color:var(--themed-color-round-tab-ghost-default-bg,#0000);border-color:var(--themed-color-round-tab-ghost-default-border,#0000);color:var(--themed-color-round-tab-ghost-default-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk{background-color:var(--themed-color-round-tab-ghost-default-bg,#0000);border-color:var(--themed-color-round-tab-ghost-default-border,#0000);color:var(--themed-color-round-tab-ghost-default-content,#dbdbdb)}@media (any-hover:hover){.roundTabButton-JbssaNvk.ghost-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-hover-bg,#0000);border-color:var(--themed-color-round-tab-ghost-hover-border,#ebebeb);color:var(--themed-color-round-tab-ghost-hover-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-hover-bg,#0000);border-color:var(--themed-color-round-tab-ghost-hover-border,#4a4a4a);color:var(--themed-color-round-tab-ghost-hover-content,#dbdbdb)}}.roundTabButton-JbssaNvk.ghost-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-ghost-active-bg,#0000);border-color:var(--themed-color-round-tab-ghost-active-border,#1a1a1a);color:var(--themed-color-round-tab-ghost-active-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-ghost-active-bg,#0000);border-color:var(--themed-color-round-tab-ghost-active-border,#fff);color:var(--themed-color-round-tab-ghost-active-content,#fff)}.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk,.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-selected-bg,#0000);border-color:var(--themed-color-round-tab-ghost-selected-border,#1a1a1a);color:var(--themed-color-round-tab-ghost-selected-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-selected-bg,#0000);border-color:var(--themed-color-round-tab-ghost-selected-border,#fff);color:var(--themed-color-round-tab-ghost-selected-content,#fff)}.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]{cursor:default}.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:after,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:after{display:none}.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled,.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:link,.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:visited,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true],.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:link,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-primary-disabled-bg,#ebebeb);border-color:var(--themed-color-round-tab-primary-disabled-bg,#ebebeb);color:var(--themed-color-round-tab-disabled-content,#b8b8b8)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:disabled,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:link,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:visited,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true],html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:link,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-primary-disabled-bg,#3d3d3d);border-color:var(--themed-color-round-tab-primary-disabled-bg,#3d3d3d);color:var(--themed-color-round-tab-disabled-content,#636363)}.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]{cursor:default}.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:after,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:after{display:none}.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled,.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:link,.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:visited,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true],.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:link,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);border-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);color:var(--themed-color-round-tab-disabled-content,#b8b8b8)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:link,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:visited,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true],html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:link,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);border-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);color:var(--themed-color-round-tab-disabled-content,#636363)}.roundTabButton-JbssaNvk.fake-JbssaNvk{left:-999999px;pointer-events:none;position:absolute;z-index:-1}.endIconWrap-JbssaNvk,.startIconWrap-JbssaNvk{align-items:center;display:inline-flex;justify-content:center;max-height:28px;max-width:28px;min-height:18px;min-width:18px}.caret-JbssaNvk{transition:transform .35s ease}.selected-JbssaNvk .caret-JbssaNvk{transform:rotate(180deg)}.visuallyHidden-JbssaNvk{border:0;height:1px;margin:-1px;padding:0;position:absolute;width:1px;clip:rect(0 0 0 0);overflow:hidden}.linkItem-zMVwkifW{cursor:pointer}.scrollWrap-vgCB17hK{contain:content;margin:-4px calc(max(var(--ui-lib-round-tabs-hor-padding, 0px), 4px)*-1);min-height:100%;padding:4px max(var(--ui-lib-round-tabs-hor-padding,0px),4px);position:relative;-webkit-user-select:none;user-select:none;width:100%}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK{overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}@supports (-moz-appearance:none){.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK{scrollbar-width:none}}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK.sb-scrollbar-wrap{display:none}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar{display:none;height:0;width:0}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar-thumb,.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar-track{display:none}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar-corner{display:none}.roundTabs-vgCB17hK{display:flex;flex-flow:row nowrap;--ui-lib-round-tab-margin:var(--ui-lib-round-tabs-gap)}.overflowScroll-vgCB17hK .roundTabs-vgCB17hK{min-width:max-content}.overflowScroll-vgCB17hK .roundTabs-vgCB17hK.center-vgCB17hK{margin-inline-end:calc(max(var(--ui-lib-round-tabs-hor-padding, 0px), 4px)*-1);padding-inline-end:max(var(--ui-lib-round-tabs-hor-padding,0px),4px)}.overflowWrap-vgCB17hK .roundTabs-vgCB17hK{gap:var(--ui-lib-round-tabs-gap);--ui-lib-round-tab-margin:0;flex-wrap:wrap}.roundTabs-vgCB17hK.start-vgCB17hK{justify-content:flex-start}.roundTabs-vgCB17hK.center-vgCB17hK{justify-content:center} \ No newline at end of file diff --git a/frontend/charting_library/bundles/116.668ae3395c34e5ab58d7.rtl.css b/frontend/charting_library/bundles/116.668ae3395c34e5ab58d7.rtl.css new file mode 100644 index 0000000..7aef3c5 --- /dev/null +++ b/frontend/charting_library/bundles/116.668ae3395c34e5ab58d7.rtl.css @@ -0,0 +1 @@ +.hidden-DgcIT6Uz{opacity:0}.fadeInWrapper-DgcIT6Uz{transition:opacity var(--ui-lib-fadeInWrapper-transition-duration,.35s);width:100%}.roundTabButton-JbssaNvk{align-items:center;border-style:solid;border-width:1px;box-sizing:border-box;cursor:default;display:inline-flex;flex:0 0 auto;justify-content:center;max-width:100%;-webkit-tap-highlight-color:transparent;outline:none;overflow:visible;position:relative;-webkit-user-select:none;user-select:none}.roundTabButton-JbssaNvk:focus{outline:none}.roundTabButton-JbssaNvk:focus-visible{outline:none}.roundTabButton-JbssaNvk:after{border-style:solid;border-width:2px;box-sizing:border-box;content:"";display:none;height:calc(100% + 10px);pointer-events:none;position:absolute;right:-5px;top:-5px;width:calc(100% + 10px);z-index:1}.roundTabButton-JbssaNvk:focus:after{display:block}.roundTabButton-JbssaNvk:focus-visible:after{display:block}.roundTabButton-JbssaNvk:focus:not(:focus-visible):after{display:none}.roundTabButton-JbssaNvk:after,html.theme-dark .roundTabButton-JbssaNvk:after{border-color:var(--themed-color-focus-outline-color-blue,#2962ff)}.roundTabButton-JbssaNvk.disableFocusOutline-JbssaNvk:after{display:none}.roundTabButton-JbssaNvk.enableCursorPointer-JbssaNvk{cursor:pointer}.roundTabButton-JbssaNvk:not(:first-child){margin-inline-start:var(--ui-lib-round-tab-margin)}.roundTabButton-JbssaNvk.large-JbssaNvk{border-radius:24px;height:48px;padding:0 23px}.roundTabButton-JbssaNvk.large-JbssaNvk:after{border-radius:28px}.roundTabButton-JbssaNvk.large-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.large-JbssaNvk.withStartIcon-JbssaNvk{padding-inline-start:15px}.roundTabButton-JbssaNvk.large-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.large-JbssaNvk.withEndIcon-JbssaNvk{padding-inline-end:15px}.roundTabButton-JbssaNvk.large-JbssaNvk:not(.iconOnly-JbssaNvk) .startIconWrap-JbssaNvk{margin-inline-end:4px}.roundTabButton-JbssaNvk.large-JbssaNvk .endIconWrap-JbssaNvk{margin-inline-start:4px}.roundTabButton-JbssaNvk.small-JbssaNvk{border-radius:17px;height:34px;padding:0 15px}.roundTabButton-JbssaNvk.small-JbssaNvk:after{border-radius:21px}.roundTabButton-JbssaNvk.small-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.small-JbssaNvk.withStartIcon-JbssaNvk{padding-inline-start:11px}.roundTabButton-JbssaNvk.small-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.small-JbssaNvk.withEndIcon-JbssaNvk{padding-inline-end:11px}.roundTabButton-JbssaNvk.small-JbssaNvk:not(.iconOnly-JbssaNvk) .startIconWrap-JbssaNvk{margin-inline-end:4px}.roundTabButton-JbssaNvk.small-JbssaNvk .endIconWrap-JbssaNvk{margin-inline-start:4px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk{border-radius:14px;height:28px;padding:0 11px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk:after{border-radius:18px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.xsmall-JbssaNvk.withStartIcon-JbssaNvk{padding-inline-start:7px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk.iconOnly-JbssaNvk,.roundTabButton-JbssaNvk.xsmall-JbssaNvk.withEndIcon-JbssaNvk{padding-inline-end:7px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk:not(.iconOnly-JbssaNvk) .startIconWrap-JbssaNvk{margin-inline-end:4px}.roundTabButton-JbssaNvk.xsmall-JbssaNvk .endIconWrap-JbssaNvk{margin-inline-start:2px}.roundTabButton-JbssaNvk.large-JbssaNvk{font-feature-settings:"tnum" on,"lnum" on;font-size:var(--ui-lib-typography-font-size);font-style:normal;font-weight:500}.roundTabButton-JbssaNvk.large-JbssaNvk,.roundTabButton-JbssaNvk.small-JbssaNvk{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;--ui-lib-typography-font-size:16px;--ui-lib-typography-line-height:24px;line-height:var(--ui-lib-typography-line-height)}.roundTabButton-JbssaNvk.small-JbssaNvk{font-feature-settings:"tnum" on,"lnum" on;font-size:var(--ui-lib-typography-font-size);font-style:normal;font-weight:400}.roundTabButton-JbssaNvk.xsmall-JbssaNvk{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:14px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:18px;line-height:var(--ui-lib-typography-line-height)}.roundTabButton-JbssaNvk.primary-JbssaNvk{background-color:var(--themed-color-round-tab-primary-default-bg,#f2f2f2);border-color:var(--themed-color-round-tab-primary-default-border,#f2f2f2);color:var(--themed-color-round-tab-primary-default-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk{background-color:var(--themed-color-round-tab-primary-default-bg,#303030);border-color:var(--themed-color-round-tab-primary-default-border,#303030);color:var(--themed-color-round-tab-primary-default-content,#dbdbdb)}@media (any-hover:hover){.roundTabButton-JbssaNvk.primary-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-primary-hover-bg,#ebebeb);border-color:var(--themed-color-round-tab-primary-hover-border,#ebebeb);color:var(--themed-color-round-tab-primary-hover-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-primary-hover-bg,#3d3d3d);border-color:var(--themed-color-round-tab-primary-hover-border,#3d3d3d);color:var(--themed-color-round-tab-primary-hover-content,#dbdbdb)}}.roundTabButton-JbssaNvk.primary-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-primary-active-bg,#dbdbdb);border-color:var(--themed-color-round-tab-primary-active-border,#dbdbdb);color:var(--themed-color-round-tab-primary-active-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-primary-active-bg,#4a4a4a);border-color:var(--themed-color-round-tab-primary-active-border,#4a4a4a);color:var(--themed-color-round-tab-primary-active-content,#fff)}.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk,.roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-primary-selected-bg,#1a1a1a);border-color:var(--themed-color-round-tab-primary-selected-border,#1a1a1a);color:var(--themed-color-round-tab-primary-selected-content,#fff)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-primary-selected-bg,#fff);border-color:var(--themed-color-round-tab-primary-selected-border,#fff);color:var(--themed-color-round-tab-primary-selected-content,#1a1a1a)}.roundTabButton-JbssaNvk.ghost-JbssaNvk{background-color:var(--themed-color-round-tab-ghost-default-bg,#0000);border-color:var(--themed-color-round-tab-ghost-default-border,#0000);color:var(--themed-color-round-tab-ghost-default-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk{background-color:var(--themed-color-round-tab-ghost-default-bg,#0000);border-color:var(--themed-color-round-tab-ghost-default-border,#0000);color:var(--themed-color-round-tab-ghost-default-content,#dbdbdb)}@media (any-hover:hover){.roundTabButton-JbssaNvk.ghost-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-hover-bg,#0000);border-color:var(--themed-color-round-tab-ghost-hover-border,#ebebeb);color:var(--themed-color-round-tab-ghost-hover-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:hover:not(:disabled):not(.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-hover-bg,#0000);border-color:var(--themed-color-round-tab-ghost-hover-border,#4a4a4a);color:var(--themed-color-round-tab-ghost-hover-content,#dbdbdb)}}.roundTabButton-JbssaNvk.ghost-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-ghost-active-bg,#0000);border-color:var(--themed-color-round-tab-ghost-active-border,#1a1a1a);color:var(--themed-color-round-tab-ghost-active-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:active:not(.disableActiveStateStyles-JbssaNvk):not(:disabled){background-color:var(--themed-color-round-tab-ghost-active-bg,#0000);border-color:var(--themed-color-round-tab-ghost-active-border,#fff);color:var(--themed-color-round-tab-ghost-active-content,#fff)}.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk,.roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-selected-bg,#0000);border-color:var(--themed-color-round-tab-ghost-selected-border,#1a1a1a);color:var(--themed-color-round-tab-ghost-selected-content,#1a1a1a)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk.selected-JbssaNvk:not(.disableActiveStateStyles-JbssaNvk){background-color:var(--themed-color-round-tab-ghost-selected-bg,#0000);border-color:var(--themed-color-round-tab-ghost-selected-border,#fff);color:var(--themed-color-round-tab-ghost-selected-content,#fff)}.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]{cursor:default}.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:after,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:after{display:none}.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled,.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:link,.roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:visited,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true],.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:link,.roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-primary-disabled-bg,#ebebeb);border-color:var(--themed-color-round-tab-primary-disabled-bg,#ebebeb);color:var(--themed-color-round-tab-disabled-content,#b8b8b8)}html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:disabled,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:link,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk:disabled:visited,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true],html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:link,html.theme-dark .roundTabButton-JbssaNvk.primary-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-primary-disabled-bg,#3d3d3d);border-color:var(--themed-color-round-tab-primary-disabled-bg,#3d3d3d);color:var(--themed-color-round-tab-disabled-content,#636363)}.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]{cursor:default}.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:after,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:after{display:none}.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled,.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:link,.roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:visited,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true],.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:link,.roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);border-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);color:var(--themed-color-round-tab-disabled-content,#b8b8b8)}html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:link,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk:disabled:visited,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true],html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:link,html.theme-dark .roundTabButton-JbssaNvk.ghost-JbssaNvk[aria-disabled=true]:visited{background-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);border-color:var(--themed-color-round-tab-ghost-disabled-bg,#0000);color:var(--themed-color-round-tab-disabled-content,#636363)}.roundTabButton-JbssaNvk.fake-JbssaNvk{pointer-events:none;position:absolute;right:-999999px;z-index:-1}.endIconWrap-JbssaNvk,.startIconWrap-JbssaNvk{align-items:center;display:inline-flex;justify-content:center;max-height:28px;max-width:28px;min-height:18px;min-width:18px}.caret-JbssaNvk{transition:transform .35s ease}.selected-JbssaNvk .caret-JbssaNvk{transform:rotate(-180deg)}.visuallyHidden-JbssaNvk{border:0;height:1px;margin:-1px;padding:0;position:absolute;width:1px;clip:rect(0 0 0 0);overflow:hidden}.linkItem-zMVwkifW{cursor:pointer}.scrollWrap-vgCB17hK{contain:content;margin:-4px calc(max(var(--ui-lib-round-tabs-hor-padding, 0px), 4px)*-1);min-height:100%;padding:4px max(var(--ui-lib-round-tabs-hor-padding,0px),4px);position:relative;-webkit-user-select:none;user-select:none;width:100%}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK{overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}@supports (-moz-appearance:none){.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK{scrollbar-width:none}}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK.sb-scrollbar-wrap{display:none}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar{display:none;height:0;width:0}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar-thumb,.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar-track{display:none}.scrollWrap-vgCB17hK.overflowScroll-vgCB17hK::-webkit-scrollbar-corner{display:none}.roundTabs-vgCB17hK{display:flex;flex-flow:row nowrap;--ui-lib-round-tab-margin:var(--ui-lib-round-tabs-gap)}.overflowScroll-vgCB17hK .roundTabs-vgCB17hK{min-width:max-content}.overflowScroll-vgCB17hK .roundTabs-vgCB17hK.center-vgCB17hK{margin-inline-end:calc(max(var(--ui-lib-round-tabs-hor-padding, 0px), 4px)*-1);padding-inline-end:max(var(--ui-lib-round-tabs-hor-padding,0px),4px)}.overflowWrap-vgCB17hK .roundTabs-vgCB17hK{gap:var(--ui-lib-round-tabs-gap);--ui-lib-round-tab-margin:0;flex-wrap:wrap}.roundTabs-vgCB17hK.start-vgCB17hK{justify-content:flex-start}.roundTabs-vgCB17hK.center-vgCB17hK{justify-content:center} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1200.04b28fea9e7b7b64a167.js b/frontend/charting_library/bundles/1200.04b28fea9e7b7b64a167.js new file mode 100644 index 0000000..e6a8ae2 --- /dev/null +++ b/frontend/charting_library/bundles/1200.04b28fea9e7b7b64a167.js @@ -0,0 +1,5 @@ +"use strict";(self.webpackChunktradingview=self.webpackChunktradingview||[]).push([[1200],{25422:(d,u,e)=>{u.transformPoint=u.translationMatrix=u.scalingMatrix=u.rotationMatrix=void 0;var f=e(86441);u.rotationMatrix=function(d){var u=Math.cos(d),e=Math.sin(d);return[[u,-e,0],[e,u,0],[0,0,1]]},u.scalingMatrix=function(d,u){return[[d,0,0],[0,u,0],[0,0,1]]},u.translationMatrix=function(d,u){return[[1,0,d],[0,1,u],[0,0,1]]},u.transformPoint=function(d,u){for(var e=[u.x,u.y,1],c=[0,0,0],t=0;t<3;t++)for(var i=0;i<3;i++)c[t]+=e[i]*d[t][i];return new f.Point(c[0],c[1])}},6590:(d,u,e)=>{e.d(u,{commonLineToolPropertiesStateKeys:()=>f});const f=["symbolStateVersion","zOrderVersion","frozen","title","interval","symbol","currencyId","unitId","visible","intervalsVisibilities.ticks","intervalsVisibilities.seconds","intervalsVisibilities.secondsFrom","intervalsVisibilities.secondsTo","intervalsVisibilities.minutes","intervalsVisibilities.minutesFrom","intervalsVisibilities.minutesTo","intervalsVisibilities.hours","intervalsVisibilities.hoursFrom","intervalsVisibilities.hoursTo","intervalsVisibilities.days","intervalsVisibilities.daysFrom","intervalsVisibilities.daysTo","intervalsVisibilities.weeks","intervalsVisibilities.weeksFrom","intervalsVisibilities.weeksTo","intervalsVisibilities.months","intervalsVisibilities.monthsFrom","intervalsVisibilities.monthsTo","intervalsVisibilities.ranges"];var c,t,i;!function(d){d[d.NotShared=0]="NotShared",d[d.SharedInLayout=1]="SharedInLayout",d[d.GloballyShared=2]="GloballyShared"}(c||(c={})),function(d){d.BeforeAllAction="BeforeAll",d.CustomAction="CustomAction"}(t||(t={})),function(d){d.FloatingToolbarButton="FloatingToolbarButton",d.Default="Default"}(i||(i={}))},38039:(d,u,e)=>{e.d(u,{LineDataSourceProperty:()=>a});var f=e(90054),c=e(16738),t=e(50151),i=e(32679);class a extends i.DefaultProperty{constructor({templateKeys:d,...u}){super({ignoreAllowSavingDefaults:!0,saveNonDefaultUserPreferencesOnly:!0,...u}),this._templateKeys=(0,t.ensureDefined)(d||this._allDefaultsKeys)}template(){return(0,i.extractState)(this.state(),this._templateKeys,[])}applyTemplate(d){this.mergeAndFire((0,i.extractState)((0,c.default)((0,f.default)(this._factoryDefaultsSupplier()),d),this._templateKeys))}}},68498:(d,u,e)=>{e.d(u,{SvgIconPaneView:()=>p});var f=e(86441),c=e(25422),t=e(50151),i=e(19063),a=e(36036),n=e(95201),r=e(27916),s=e(19625),o=e(75919),l=e(56468);const b=(0,s.getHexColorByName)("color-tv-blue-600");class h extends o.MediaCoordinatesPaneRenderer{constructor(){super(...arguments),this._data=null}setData(d){this._data=d}hitTest(d){if(null===this._data)return null;const{size:u,angle:e,point:f}=this._data,t=(0,c.rotationMatrix)(-e),i=(0,c.transformPoint)(t,d.subtract(f));return Math.abs(i.y)<=u/2&&Math.abs(i.x)<=u/2?new l.HitTestResult(l.HitTarget.MovePoint):null}isOutOfScreen(d,u){if(null===this._data)return!0;const{size:e,point:f,angle:c}=this._data;let t;return t=c%(Math.PI/2)==0?e/2:Math.sqrt(e**2*2)/2,f.x+t<0||f.x-t>u||f.y+t<0||f.y-t>d}_drawImpl(d){if(null===this._data)return +;const{size:u,svg:e,point:f,angle:c,color:t,background:i,selected:a}=this._data,n=d.context;n.translate(f.x,f.y);const r=c-Math.PI/2;n.rotate(r);const s=u/2;a&&(n.fillStyle=i,n.strokeStyle=b,n.beginPath(),n.rect(-s,-s,u,u),n.closePath(),n.fill(),n.stroke()),e&&(n.translate(-s,-s),null!==t&&(n.fillStyle=t),e.render(n,{targetViewBox:{x:0,y:0,width:u,height:u},doNotApplyColors:null!==t}))}}class p extends r.LineSourcePaneView{constructor(d,u,e){super(d,u),this._iconRenderer=new h,this._renderer=new n.CompositeRenderer,this._svg=e}renderer(d){return this._invalidated&&this._updateImpl(d),this._renderer}_updateImpl(d){if(super._updateImpl(d),this._renderer.clear(),this._points.length<1)return;const u=this._source.properties().childs(),e=u.size.value(),t={point:this._points[0],color:this._iconColor(),size:e,svg:this._svg,angle:u.angle.value(),selected:this.areAnchorsVisible(),background:this._calculateBackgroundColor()};this._iconRenderer.setData(t);const{mediaSize:{width:i,height:n}}=d;this._iconRenderer.isOutOfScreen(n,i)||this._renderer.append(this._iconRenderer);const[s]=this._points,o=this._source.getAnchorLimit();let l=new f.Point(Math.max(o,e)/2,0),b=new f.Point(0,Math.max(o,e)/2);const h=(0,c.rotationMatrix)(u.angle.value());l=(0,c.transformPoint)(h,l),b=(0,c.transformPoint)(h,b);const p=s.add(l),v=s.subtract(l),m=(0,r.thirdPointCursorType)(p,v),g=[(0,a.lineSourcePaneViewPointToLineAnchorPoint)(p,0,void 0,void 0,void 0,void 0,void 0,!0),(0,a.lineSourcePaneViewPointToLineAnchorPoint)(v,1,void 0,void 0,void 0,void 0,void 0,!0),(0,a.lineSourcePaneViewPointToLineAnchorPoint)(s.add(b),2,m,!0,void 0,void 0,void 0,!0),(0,a.lineSourcePaneViewPointToLineAnchorPoint)(s.subtract(b),3,m,!0,void 0,void 0,void 0,!0)];this._renderer.append(this.createLineAnchor({points:g},0))}_calculateBackgroundColor(){return(0,i.generateColor)(this._model.backgroundColorAtYPercentFromTop(this._points[0].y/(0,t.ensureNotNull)(this._model.paneForSource(this._source)).height()),60,!0)}}},19365:(d,u,e)=>{e.d(u,{getTwemojiUrl:()=>c});var f=e(18438);function c(d,u){let e="";return f.default.parse(d,(d=>(e=f.default.base+("svg"===u?`svg/${d}.svg`:`72x72/${d}.png`),!1))),e}f.default.base="https://cdnjs.cloudflare.com/ajax/libs/twemoji/13.0.1/"},18438:(d,u,e)=>{e.d(u,{default:()=>f});const f=function(){var d={base:"https://twemoji.maxcdn.com/v/13.0.1/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:function(d){var u="string"==typeof d?parseInt(d,16):d;if(u<65536)return a(u);return a(55296+((u-=65536)>>10),56320+(1023&u))},toCodePoint:m},onerror:function(){this.parentNode&&this.parentNode.replaceChild(n(this.alt,!1),this)},parse:function(u,e){e&&"function"!=typeof e||(e={callback:e});return("string"==typeof u?b:l)(u,{callback:e.callback||r,attributes:"function"==typeof e.attributes?e.attributes:p,base:"string"==typeof e.base?e.base:d.base,ext:e.ext||d.ext,size:e.folder||(f=e.size||d.size,"number"==typeof f?f+"x"+f:f),className:e.className||d.className,onerror:e.onerror||d.onerror});var f},replace:v,test:function(d){e.lastIndex=0 +;var u=e.test(d);return e.lastIndex=0,u}},u={"&":"&","<":"<",">":">","'":"'",'"':""" +},e=/(?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udeeb\udeec\udef4-\udefc\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78\udd7a-\uddb4\uddb7\uddba\uddbc-\uddcb\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7a\ude80-\ude86\ude90-\udea8\udeb0-\udeb6\udec0-\udec2\uded0-\uded6]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g,f=/\uFE0F/g,c=String.fromCharCode(8205),t=/[&<>'"]/g,i=/^(?:iframe|noframes|noscript|script|select|style|textarea)$/,a=String.fromCharCode +;return d;function n(d,u){return document.createTextNode(u?d.replace(f,""):d)}function r(d,u){return"".concat(u.base,u.size,"/",d,u.ext)}function s(d,u){for(var e,f,c=d.childNodes,t=c.length;t--;)3===(f=(e=c[t]).nodeType)?u.push(e):1!==f||"ownerSVGElement"in e||i.test(e.nodeName.toLowerCase())||s(e,u);return u}function o(d){return m(d.indexOf(c)<0?d.replace(f,""):d)}function l(d,u){for(var f,c,t,i,a,r,l,b,h,p,v,m,g,x=s(d,[]),y=x.length;y--;){for(t=!1,i=document.createDocumentFragment(),r=(a=x[y]).nodeValue,b=0;l=e.exec(r);){if((h=l.index)!==b&&i.appendChild(n(r.slice(b,h),!0)),m=o(v=l[0]),b=h+v.length,g=u.callback(m,u),m&&g){for(c in(p=new Image).onerror=u.onerror,p.setAttribute("draggable","false"),f=u.attributes(v,m))f.hasOwnProperty(c)&&0!==c.indexOf("on")&&!p.hasAttribute(c)&&p.setAttribute(c,f[c]);p.className=u.className,p.alt=v,p.src=g,t=!0,i.appendChild(p)}p||i.appendChild(n(v,!1)),p=null}t&&(b")}return c}))}function h(d){return u[d]}function p(){return null}function v(d,u){return String(d).replace(e,u)}function m(d,u){for(var e=[],f=0,c=0,t=0;t{var n;!function(r,s,o,a){"use strict";var h,u=["","webkit","Moz","MS","ms","o"],c=s.createElement("div"),l=Math.round,p=Math.abs,f=Date.now;function v(t,e,i){return setTimeout(I(t,i),e)}function d(t,e,i){return!!Array.isArray(t)&&(m(t,i[e],i),!0)}function m(t,e,i){var n;if(t)if(t.forEach)t.forEach(e,i);else if(t.length!==a)for(n=0;n\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",s=r.console&&(r.console.warn||r.console.log);return s&&s.call(r.console,n,i),t.apply(this,arguments)}}h="function"!=typeof Object.assign?function(t){if(t===a||null===t)throw new TypeError("Cannot convert undefined or null to object");for(var e=Object(t),i=1;i-1}function D(t){return t.trim().split(/\s+/g)}function w(t,e,i){if(t.indexOf&&!i)return t.indexOf(e);for(var n=0;ni[e]})):n.sort()),n}function R(t,e){for(var i,n,r=e[0].toUpperCase()+e.slice(1),s=0;s1&&!i.firstMultiple?i.firstMultiple=V(e):1===r&&(i.firstMultiple=!1);var s=i.firstInput,o=i.firstMultiple,h=o?o.center:s.center,u=e.center=j(n);e.timeStamp=f(),e.deltaTime=e.timeStamp-s.timeStamp,e.angle=$(h,u),e.distance=B(h,u),function(t,e){var i=e.center,n=t.offsetDelta||{},r=t.prevDelta||{},s=t.prevInput||{};1!==e.eventType&&4!==s.eventType||(r=t.prevDelta={x:s.deltaX||0,y:s.deltaY||0},n=t.offsetDelta={x:i.x,y:i.y});e.deltaX=r.x+(i.x-n.x),e.deltaY=r.y+(i.y-n.y)}(i,e),e.offsetDirection=Z(e.deltaX,e.deltaY);var c=G(e.deltaTime,e.deltaX,e.deltaY);e.overallVelocityX=c.x,e.overallVelocityY=c.y,e.overallVelocity=p(c.x)>p(c.y)?c.x:c.y,e.scale=o?(l=o.pointers,v=n,B(v[0],v[1],L)/B(l[0],l[1],L)):1,e.rotation=o?function(t,e){return $(e[1],e[0],L)+$(t[1],t[0],L)}(o.pointers,n):0,e.maxPointers=i.prevInput?e.pointers.length>i.prevInput.maxPointers?e.pointers.length:i.prevInput.maxPointers:e.pointers.length,function(t,e){var i,n,r,s,o=t.lastInterval||e,h=e.timeStamp-o.timeStamp;if(8!=e.eventType&&(h>25||o.velocity===a)){var u=e.deltaX-o.deltaX,c=e.deltaY-o.deltaY,l=G(h,u,c);n=l.x,r=l.y,i=p(l.x)>p(l.y)?l.x:l.y,s=Z(u,c),t.lastInterval=e}else i=o.velocity,n=o.velocityX,r=o.velocityY,s=o.direction;e.velocity=i,e.velocityX=n,e.velocityY=r,e.direction=s}(i,e);var l,v;var d=t.element;S(e.srcEvent.target,d)&&(d=e.srcEvent.target);e.target=d}(t,i),t.emit("hammer.input",i),t.recognize(i),t.session.prevInput=i}function V(t){for(var e=[],i=0;i=p(e)?t<0?2:4:e<0?8:16}function B(t,e,i){i||(i=q);var n=e[i[0]]-t[i[0]],r=e[i[1]]-t[i[1]];return Math.sqrt(n*n+r*r)}function $(t,e,i){i||(i=q);var n=e[i[0]]-t[i[0]],r=e[i[1]]-t[i[1]];return 180*Math.atan2(r,n)/Math.PI}H.prototype={handler:function(){},init:function(){this.evEl&&b(this.element,this.evEl,this.domHandler),this.evTarget&&b(this.target,this.evTarget,this.domHandler),this.evWin&&b(z(this.element),this.evWin,this.domHandler)},destroy:function(){this.evEl&&_(this.element,this.evEl,this.domHandler),this.evTarget&&_(this.target,this.evTarget,this.domHandler),this.evWin&&_(z(this.element),this.evWin,this.domHandler)}};var J={mousedown:1,mousemove:2,mouseup:4},K="mousedown",Q="mousemove mouseup";function tt(){this.evEl=K,this.evWin=Q,this.pressed=!1,H.apply(this,arguments)}E(tt,H,{handler:function(t){var e=J[t.type];1&e&&0===t.button&&(this.pressed=!0),2&e&&1!==t.which&&(e=4),this.pressed&&(4&e&&(this.pressed=!1),this.callback(this.manager,e,{pointers:[t],changedPointers:[t],pointerType:k,srcEvent:t}))}});var et={pointerdown:1,pointermove:2,pointerup:4,pointercancel:8,pointerout:8},it={2:F,3:"pen",4:k, +5:"kinect"},nt="pointerdown",rt="pointermove pointerup pointercancel";function st(){this.evEl=nt,this.evWin=rt,H.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}r.MSPointerEvent&&!r.PointerEvent&&(nt="MSPointerDown",rt="MSPointerMove MSPointerUp MSPointerCancel"),E(st,H,{handler:function(t){var e=this.store,i=!1,n=t.type.toLowerCase().replace("ms",""),r=et[n],s=it[t.pointerType]||t.pointerType,o=s==F,a=w(e,t.pointerId,"pointerId");1&r&&(0===t.button||o)?a<0&&(e.push(t),a=e.length-1):12&r&&(i=!0),a<0||(e[a]=t,this.callback(this.manager,r,{pointers:e,changedPointers:[t],pointerType:s,srcEvent:t}),i&&e.splice(a,1))}});var ot={touchstart:1,touchmove:2,touchend:4,touchcancel:8};function at(){this.evTarget="touchstart",this.evWin="touchstart touchmove touchend touchcancel",this.started=!1,H.apply(this,arguments)}function ht(t,e){var i=x(t.touches),n=x(t.changedTouches);return 12&e&&(i=O(i.concat(n),"identifier",!0)),[i,n]}E(at,H,{handler:function(t){var e=ot[t.type];if(1===e&&(this.started=!0),this.started){var i=ht.call(this,t,e);12&e&&i[0].length-i[1].length==0&&(this.started=!1),this.callback(this.manager,e,{pointers:i[0],changedPointers:i[1],pointerType:F,srcEvent:t})}}});var ut={touchstart:1,touchmove:2,touchend:4,touchcancel:8},ct="touchstart touchmove touchend touchcancel";function lt(){this.evTarget=ct,this.targetIds={},H.apply(this,arguments)}function pt(t,e){var i=x(t.touches),n=this.targetIds;if(3&e&&1===i.length)return n[i[0].identifier]=!0,[i,i];var r,s,o=x(t.changedTouches),a=[],h=this.target;if(s=i.filter((function(t){return S(t.target,h)})),1===e)for(r=0;r-1&&n.splice(t,1)}),2500)}}function mt(t){for(var e=t.srcEvent.clientX,i=t.srcEvent.clientY,n=0;n-1&&this.requireFail.splice(e,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(t){return!!this.simultaneous[t.id]},emit:function(t){ +var e=this,i=this.state;function n(i){e.manager.emit(i,t)}i<8&&n(e.options.event+wt(i)),n(e.options.event),t.additionalEvent&&n(t.additionalEvent),i>=8&&n(e.options.event+wt(i))},tryEmit:function(t){if(this.canEmit())return this.emit(t);this.state=Pt},canEmit:function(){for(var t=0;te.threshold&&r&e.direction},attrTest:function(t){return Rt.prototype.attrTest.call(this,t)&&(2&this.state||!(2&this.state)&&this.directionTest(t))},emit:function(t){this.pX=t.deltaX,this.pY=t.deltaY;var e=xt(t.direction);e&&(t.additionalEvent=this.options.event+e),this._super.emit.call(this,t)}}),E(zt,Rt,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[At]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.scale-1)>this.options.threshold||2&this.state)},emit:function(t){if(1!==t.scale){var e=t.scale<1?"in":"out";t.additionalEvent=this.options.event+e}this._super.emit.call(this,t)}}),E(Nt,Dt,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[Et]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,n=t.distancee.time;if(this._input=t,!n||!i||12&t.eventType&&!r)this.reset();else if(1&t.eventType)this.reset(),this._timer=v((function(){this.state=8,this.tryEmit()}),e.time,this);else if(4&t.eventType)return 8;return Pt},reset:function(){clearTimeout(this._timer)},emit:function(t){8===this.state&&(t&&4&t.eventType?this.manager.emit(this.options.event+"up",t):(this._input.timeStamp=f(),this.manager.emit(this.options.event,this._input)))}}),E(Xt,Rt,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[At]},attrTest:function(t){return this._super.attrTest.call(this,t)&&(Math.abs(t.rotation)>this.options.threshold||2&this.state)}}),E(Yt,Rt,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:30,pointers:1},getTouchAction:function(){return Mt.prototype.getTouchAction.call(this)},attrTest:function(t){var e,i=this.options.direction +;return 30&i?e=t.overallVelocity:6&i?e=t.overallVelocityX:i&W&&(e=t.overallVelocityY),this._super.attrTest.call(this,t)&&i&t.offsetDirection&&t.distance>this.options.threshold&&t.maxPointers==this.options.pointers&&p(e)>this.options.velocity&&4&t.eventType},emit:function(t){var e=xt(t.offsetDirection);e&&this.manager.emit(this.options.event+e,t),this.manager.emit(this.options.event,t)}}),E(Ft,Dt,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[It]},process:function(t){var e=this.options,i=t.pointers.length===e.pointers,n=t.distance{i.d(t,{LineToolRiskRewardBase:()=>N,registerReversibleTool:()=>E,roundValue:()=>M});var s=i(50279),r=i(50151),n=i(11542),o=i(8025),c=i(93280),a=i(74079),l=i(29875),u=i(76386),h=i(76050),d=i(98558),p=i(19063);class _ extends d.PriceAxisView{constructor(e,t){super(),this._source=e,this._data=t}_updateRendererData(e,t,i){if(e.visible=!1,!this._showAxisLabel())return;const s=this._source.priceScale();if(0===this._source.points().length||null===s||s.isEmpty())return;const r=this._source.ownerSource(),n=null!==r?r.firstValue():null;if(null===n)return;const o=this._data.priceProperty.value(),c=(0,p.resetTransparency)(this._data.colorProperty.value());i.background=c,i.textColor=this.generateTextColor(c),i.coordinate=s.priceToCoordinate(o,n),e.text=s.formatPrice(o,n),e.visible=!0}_showAxisLabel(){return this._source.properties().childs().showPriceLabels.value()||this._source.model().selection().isSelected(this._source)}}var y=i(12988);class P extends y.Property{constructor(e,t){super(),this._lineSource=e,this._pointIndex=t}value(){const e=this._lineSource.points(),t=e[this._pointIndex]?e[this._pointIndex].price:this._lineSource.normalizedPoints()[this._pointIndex].price;return this._formatAndParsePrice(t)}state(){return this.value()}merge(e,t){return this.setValue(e),t?[]:null}_formatAndParsePrice(e){const t=(0,r.ensureNotNull)(this._lineSource.ownerSource()),i=t.defaultFormatter?.()||t.formatter();if(i.parse){const t=i.format(e),s=i.parse(t);return s.res?s.value:e}return e}}class S extends P{constructor(e){super(e,0)}setValue(e){if(this._lineSource.isSourceHidden()){const t=this._lineSource.normalizedPoints();return t[this._pointIndex].price=parseFloat(e.toString()),void this._lineSource.restorePoints(t,[])}const t=this._lineSource.points()[this._pointIndex];this._lineSource.startChanging(this._pointIndex,t),t.price=parseFloat(e.toString()),this._lineSource.setPoint(this._pointIndex,t),this._lineSource.recalculate(),this._lineSource.model().updateSource(this._lineSource),this._listeners.fire(this,""),this._lineSource.endChanging(!1,!1),this._lineSource.syncPriceLevels()}}class v extends P{constructor(e){super(e,1)}value(){const e=this._lineSource.stopPrice();return this._formatAndParsePrice(e)}setValue(e){const t=Math.round(Math.abs(e-this._lineSource.entryPrice())*this._lineSource.ownerSourceBase());this._lineSource.properties().childs().stopLevel.setValue(t),this._lineSource.syncPriceLevels()}}class m extends P{constructor(e){super(e,2)}value(){const e=this._lineSource.profitPrice();return this._formatAndParsePrice(e)}setValue(e){const t=Math.round(Math.abs(e-this._lineSource.entryPrice())*this._lineSource.ownerSourceBase());this._lineSource.properties().childs().profitLevel.setValue(t),this._lineSource.syncPriceLevels()}}var f,g=i(37265),b=i(928),x=i(44672),w=i(77148),C=i(29023),k=i(45126),R=i(64147),L=i(92184),V=i(29137),A=i(19466);function M(e){return parseFloat(e.toFixed(2))}!function(e){ +e[e.InitialVersion=1]="InitialVersion",e[e.CurrentVersion=2]="CurrentVersion"}(f||(f={}));const I=new Map;function E(e,t){I.set(e,t)}const z=new k.TranslatedString("reverse {tool}",n.t(null,void 0,i(66643)));class N extends l.LineDataSource{constructor(e,t,s,n){super(e,t,s,n),this._hasEditableCoordinates=new R.WatchedValue(!1),this._studySource=null,this._metaInfo=null,this._riskInChange=!1,this._syncStateExclusions.push("points","entryPrice","stopPrice","targetPrice","stopLevel","profitLevel","riskSize","qty","amountTarget","amountStop");const o=this._metaInfo?.inputs.find((e=>"account_currency"===e.id)),a=t.childs().currency.value();o&&!o.options.includes(a)&&t.childs().currency.setValue(o.defval),o&&t.childs().currency.subscribe(this,this._updateStudySource),this.version=2,t.hasChild("stopLevel")||t.hasChild("profitLevel")||(t.addProperty("stopLevel",0),t.addProperty("profitLevel",0),this.ownerSourceChanged().subscribe(this,(()=>{const i=(0,r.ensureNotNull)(e.timeScale().visibleBarsStrictRange()),s=i.firstBar(),n=i.lastBar(),o=(0,r.ensureNotNull)(this.ownerSource()),a=o.priceScale();if(a){let e=(0,r.ensureNotNull)(o.priceRange(s,n,{targetPriceScale:a,scaleSeriesOnly:a?.isScaleSeriesOnly()}));if(a.isLog()){const t=a.logicalToPrice(e.minValue()),i=a.logicalToPrice(e.maxValue());e=new c.PriceRange(t,i)}if(e&&!e.isEmpty()){const i=Math.round(.2*e.length()*this.ownerSourceBase());t.merge({stopLevel:i,profitLevel:i})}}}),!0)),t.hasChild("entryPointCurrencyRate")||t.addProperty("entryPointCurrencyRate",1),t.hasChild("closePointCurrencyRate")||t.addProperty("closePointCurrencyRate",1);const l=t.childs();l.stopLevel.subscribe(this,this.recalculate),l.stopLevel.subscribe(null,(()=>{this.properties().childs().stopPrice.fireChanged()})),l.profitLevel.subscribe(this,this.recalculate),l.profitLevel.subscribe(null,(()=>{this.properties().childs().targetPrice.fireChanged()})),t.addChild("entryPrice",new S(this)),t.addChild("stopPrice",new v(this)),t.addChild("targetPrice",new m(this)),t.hasChild("riskSize")||t.addProperty("riskSize",0),t.hasChild("qty")||t.addProperty("qty",0),t.hasChild("amountTarget")||t.addProperty("amountTarget",l.accountSize.value()),t.hasChild("amountStop")||t.addProperty("amountStop",l.accountSize.value()),["riskSize","qty","amountTarget","amountStop","currency","entryPointCurrencyRate","closePointCurrencyRate"].forEach((e=>{t.addExcludedKey(e,1)})),["qty","amountTarget","amountStop","currency","entryPointCurrencyRate","closePointCurrencyRate"].forEach((e=>{t.addExcludedKey(e,4)})),l.risk.subscribe(this,this._recalculateRiskSize),l.accountSize.subscribe(this,this._recalculateRiskSize),l.riskDisplayMode.subscribe(this,this._recalculateRisk),l.riskDisplayMode.subscribe(this,this._recalculateRiskSize),l.entryPrice.subscribe(this,this._recalculateRiskSize),l.stopPrice.subscribe(this,this._recalculateRiskSize),l.profitLevel.subscribe(this,this._recalculateRiskSize),l.profitLevel.subscribe(this,this.syncPriceLevels.bind(this)),l.stopLevel.subscribe(this,this._recalculateRiskSize), +l.stopLevel.subscribe(this,this.syncPriceLevels.bind(this)),l.qty.subscribe(this,this._recalculateRiskSize),this.ownerSourceChanged().subscribe(null,((e,t)=>{e&&e.barsProvider().dataUpdated().unsubscribeAll(this),t&&t.barsProvider().dataUpdated().subscribe(this,this._onSeriesUpdated)})),this.pointAdded().subscribe(this,(e=>{switch(e){case h.RiskRewardPointIndex.Entry:case h.RiskRewardPointIndex.Close:this._recalculateRiskSize(),this._recalculateQty()}this._updateStudySource()})),this.pointChanged().subscribe(this,(e=>{switch(e){case h.RiskRewardPointIndex.Entry:case h.RiskRewardPointIndex.Close:this._recalculateRiskSize(),this._recalculateQty()}this._updateStudySource()})),l.riskDisplayMode.value()===u.RiskDisplayMode.Percentage&&l.risk.value()>100&&l.riskDisplayMode.setValueSilently(u.RiskDisplayMode.Money),l.entryPrice.subscribe(this,this._recalculateQty),l.stopPrice.subscribe(this,this._recalculateQty),l.riskSize.subscribe(this,this._recalculateQty),l.entryPrice.subscribe(this,this._recalculateAmount),l.profitLevel.subscribe(this,this._recalculateAmount),l.stopLevel.subscribe(this,this._recalculateAmount),l.accountSize.subscribe(this,this._recalculateAmount),l.riskSize.subscribe(this,this._recalculateAmount),l.qty.subscribe(this,this._recalculateAmount),this._entryPriceAxisView=new _(this,{colorProperty:l.linecolor,priceProperty:l.entryPrice}),this._stopPriceAxisView=new _(this,{colorProperty:l.stopBackground,priceProperty:l.stopPrice}),this._profitPriceAxisView=new _(this,{colorProperty:l.profitBackground,priceProperty:l.targetPrice}),[l.entryPointCurrencyRate,l.closePointCurrencyRate].forEach((e=>{e.subscribe(this,(()=>{this._recalculateAmount(),this._recalculateRiskSize(),this._recalculateQty()}))})),Promise.all([i.e(6290),i.e(9116),i.e(1200),i.e(1583)]).then(i.bind(i,1971)).then((({RiskRewardPaneView:t})=>{const i=[new t(this,e)];this._setPaneViews(i)}))}destroy(){this.ownerSource()?.barsProvider().dataUpdated().unsubscribeAll(this),this.ownerSourceChanged().unsubscribeAll(this),this._studySource?.destroy(),super.destroy()}availableCurrencies(){const e=(0,r.ensureDefined)(this._metaInfo?.inputs.find((e=>"account_currency"===e.id)));return(0,r.ensureDefined)(e.options)}setOwnerSource(e){super.setOwnerSource(e);const t=this.ownerSource();t&&t.symbolSource().symbolInfo()&&(this._recalculateAmount(),this._recalculateRiskSize(),this._recalculateQty())}pointsCount(){return 2}priceAxisPoints(){if(!this._points.length)return[];const e=this._points[0],t=this._properties.childs();return[{...e,price:t.stopPrice.value()},{...e,price:t.entryPrice.value()},{...e,price:t.targetPrice.value()}]}priceAxisViews(e,t){return this.isSourceHidden()||t!==this.priceScale()||this._model.paneForSource(this)!==e?null:[this._entryPriceAxisView,this._stopPriceAxisView,this._profitPriceAxisView]}updateAllViews(e){this.isActualSymbol()&&this.properties().childs().visible.value()&&((0,b.hideAllDrawings)().value()&&this.userEditEnabled()||(super.updateAllViews(e),this._entryPriceAxisView.update(e),this._stopPriceAxisView.update(e), +this._profitPriceAxisView.update(e)))}migrateVersion(e,t,i){if(1===e&&this._points.length>=1){const e=[];e.push(this._points[0]);let t=this._points[0];if(t={price:t.price,index:this._getClosePointIndex(t.index),interval:this._properties.childs().interval.value()},e.push(t),this._points[1]&&e.push(this._points[1]),this._points[2]&&e.push(this._points[2]),this._points=e,this._timePoint.length>=1){const t=[],i=this._timePoint[0];t.push(i);const s={price:i.price,time_t:i.time_t,offset:this._getClosePointIndex(i.offset)};t.push(s),this._timePoint[1]&&e.push(this._points[1]),this._timePoint[2]&&e.push(this._points[2]),this._timePoint=t}}}restoreExternalState(e){if(!(0,g.isNumber)(e.entryPrice))return void super.restoreExternalState(e);let t;if(this.isActualSymbol())t=e;else{const{entryPrice:i,...s}=e,[r]=this._timePoint;r.price=i,t=s}this.properties().merge(t)}addPoint(e,t,i){e.price=this._roundPrice(e.price),super.addPoint(e,void 0,!0);const s={price:e.price,index:this._getClosePointIndex(e.index)};super._addPointIntenal(s,void 0,!0);const r=this._calculateActualEntry(e,s);if(r){super._addPointIntenal(r,void 0,!0);const e=this._findClosePoint(r,s);e&&super._addPointIntenal(e,void 0,!0)}return this._lastPoint=null,this._normalizePoints(),this.createServerPoints(),!0}setPoint(e,t,i,s){if(this.isSourceHidden())return;const r=this.properties().childs();switch(this._muteSyncLineStyle(),e){case 0:this._changeEntryPoint({...t,interval:this._model.mainSeries().interval()});break;case 2:r.stopPrice.setValue(this.prepareStopPrice(t.price));break;case 3:r.targetPrice.setValue(this.prepareProfitPrice(t.price));break;case 1:t.price=this._points[0].price,super.setPoint(1,t),this.recalculate()}this._unmuteSyncLineStyleWithoutApplyingChanges(),s||this.syncPriceLevels()}getPoint(e){if(this.isSourceHidden())return null;switch(e){case 0:return this._points[0];case 1:return{index:this._points[1].index,price:this._points[0].price};case 2:return{index:this._points[0].index,price:this.stopPrice()};case 3:return{index:this._points[0].index,price:this.profitPrice()}}return null}setPoints(e){this.isSourceHidden()||(this._muteSyncLineStyle(),super.setPoints(e),this.recalculate(),this._unmuteSyncLineStyleWithoutApplyingChanges(),this.syncPriceLevels())}start(){super.start(),this.recalculate()}startMoving(e,t,i,s){const n=(0,r.ensureDefined)(e.logical);n.price=this._roundPrice(n.price),super.startMoving(e,t,i)}move(e,t,i,s){const n=(0,r.ensureDefined)(e.logical);n.price=this._roundPrice(n.price),super.move(e,t,i),this.recalculate(),this._entryPriceAxisView.update((0,x.sourceChangeEvent)(this.id()))}axisPoints(){if(!this._points[h.RiskRewardPointIndex.ActualEntry])return[];const e=this._points[h.RiskRewardPointIndex.ActualEntry];let t=null;if(4===this._points.length)t=this._points[h.RiskRewardPointIndex.ActualClose];else{const e=this.lastBarData();if(!e)return[];t={index:e.index,price:e.closePrice}}return[e,t]}recalculateStateByData(){this.recalculate()}recalculate(){if(0===this.points().length)return +;const e=this.properties().childs(),t=e.targetPrice.value(),i=e.stopPrice.value(),s=[this._points[0],this._points[1]],r=this._calculateActualEntry(this.points()[0],this.points()[1]),n=this._model.mainSeries().interval();if(r){s.push({...r,interval:n});const e=this._findClosePoint(r,this.points()[1]);e&&s.push({...e,interval:n})}this._points=s,t!==e.targetPrice.value()&&e.targetPrice.fireChanged(),i!==e.stopPrice.value()&&e.stopPrice.fireChanged()}syncPriceLevels(){const e=this.linkKey().value();if(e){const t=this.properties().childs();this._syncLineStyleChanges(e,{entryPrice:t.entryPrice.value(),stopLevel:t.stopLevel.value(),profitLevel:t.profitLevel.value()})}}entryPrice(){const e=this.points();return 0===e.length?this._timePoint[0].price:e[0].price}lastBarData(){const e=this.ownerSource()?.barsProvider();if(!e)return null;const t=e.bars().firstIndex(),i=e.bars().lastIndex();if(null===t||null===i||isNaN(t)||isNaN(i))return null;const s=this.points();if(4===s.length){const e=s[h.RiskRewardPointIndex.ActualClose];return e.index{const t=z.format({tool:(0,V.getTranslatedStringForSource)(A.TitleDisplayTarget.StatusLine,this)});e.beginUndoMacro(t);const i=function(e,t){const i=e.points(),s=e.zorder(),n=(0,r.ensureNotNull)(t.model().paneForSource(e)),o=(0,r.ensureNotNull)(e.ownerSource()),c=(0,r.ensureDefined)(I.get(e.toolname)),a=c.propertiesFactory(t.model().backgroundTheme().spawnOwnership(),e.properties().state()),l=e.linkKey().value(),u=l?w.CreateLineToolSyncMode.ForceOn:w.CreateLineToolSyncMode.ForceOff,h=t.createLineTool({pane:n,point:i[0],linkKey:l?(0,L.randomHash)():void 0,linetool:c.targetToolName,properties:a,ownerSource:o,synchronizationMode:u,sharingMode:e.sharingMode().value()});return h&&(t.startChangingLinetool(h,h.points()[1],1),t.changeLinePoint(i[1]),t.endChangingLinetool(!1),t.insertBefore([h],e),h.setZorder(s)),t.removeSource(e,!1),h}(this,e);e.endUndoMacro(),i&&e.model().selectionMacro((e=>{e.addSourceToSelection(i,null)}))}}})],placement:"CustomAction"}}_entryPointCurrencyRate(){return this.properties().childs().entryPointCurrencyRate.value()}_closePointCurrencyRate(){return this.properties().childs().closePointCurrencyRate.value()}_ignoreSourceEvent(e){return super._ignoreSourceEvent(e)&&e.sourceId!==this._model.mainSeries().id()}_applyTemplateImpl(e){ +const{targetPrice:t,stopPrice:i,entryPrice:s,...r}=e;super._applyTemplateImpl(r);const n=this.properties().childs();void 0!==e.stopLevel&&n.stopLevel.setValue(e.stopLevel),void 0!==e.profitLevel&&n.profitLevel.setValue(e.profitLevel)}_propertiesStateExclusions(){return[...super._propertiesStateExclusions(),"entryPrice","stopPrice","targetPrice"]}_correctPoints(e,t){if(!this.isActualSymbol())return!1;const i=super._correctPoints(e.slice(0,this.pointsCount()),t);for(let t=0;ts?(e.risk.setValue(s),e.riskSize.setValue(s)):e.riskSize.setValue(t)}_roundPrice(e){const t=this.ownerSourceBase();return Math.round(e*t)/t}_ownerSourcePointValue(){return this.ownerSource()?.symbolSource().symbolInfo()?.pointvalue??1}_onSourceHiddenMayChange(){super._onSourceHiddenMayChange(),this._updateStudySource()}static _configureProperties(e){l.LineDataSource._configureProperties(e),e.addExcludedKey("stopLevel",1),e.addExcludedKey("profitLevel",1),e.addExcludedKey("stopPrice",1),e.addExcludedKey("targetPrice",1),e.addExcludedKey("entryPrice",1)}_onSeriesUpdated(e,t,i){this.isSourceHidden()||this._points.length<2||null!==i&&i.index>Math.max(this._points[0].index,this._points[1].index)||this.recalculateStateByData()}_recalculateRisk(){const e=this.properties().childs(),t=e.riskDisplayMode.value(),i=e.riskSize.value(),s=e.accountSize.value();let r=e.risk.value();r=t===u.RiskDisplayMode.Percentage?M(i/s*100):M(s/100*r),this._riskInChange=!0,e.risk.setValue(parseFloat(this._riskFormatter(t).format(r,{ignoreLocaleNumberFormat:!0}))),this._riskInChange=!1}_recalculateAmount(){if(0===this.points().length)return;const e=this.properties().childs(),t=e.accountSize.value(),i=e.entryPrice.value(),s=e.qty.value(),r=e.stopPrice.value(),n=e.targetPrice.value(),o=this._ownerSourcePointValue();e.amountTarget.setValue(this._amountTarget(t,n,i,s,o)),e.amountStop.setValue(this._amountStop(t,r,i,s,o))}_recalculateQty(){if(0===this.points().length)return;const e=this.properties().childs(),t=e.entryPrice.value(),i=e.stopPrice.value(),s=e.riskSize.value(),r=this._entryPointCurrencyRate(),n=null===r?NaN:s/(Math.abs(t-i)*this._ownerSourcePointValue()*r);e.qty.setValue(n)}_calculateActualEntry(e,t){const i=this.ownerSource();if(!i)return null;const s=i.barsProvider().bars();if(s.isEmpty())return null;const n=(0,r.ensureNotNull)(s.firstIndex()),o=Math.max(e.index,n),c=e.price,a=(0,r.ensureNotNull)(s.lastIndex()),l=Math.min(a,t.index-1);for(const e of s.rangeIterator(o,l+1)){const t=e.value;if(null!==t&&(0,r.ensure)(t[2])>=c&&(0,r.ensure)(t[3])<=c)return{index:e.index,price:c}}return null}_riskFormatter(e){ +return e===u.RiskDisplayMode.Percentage?(0,a.getNumericFormatter)(2):(0,a.getNumericFormatter)()}_getClosePointIndex(e){const t=this._model.timeScale(),i=Math.round(t.width()/t.barSpacing());return e+Math.max(3,Math.round(.15*i))}_findClosePoint(e,t){const i=this.ownerSource();if(!i)return null;const s=i.barsProvider().bars(),n=(0,r.ensureNotNull)(s.firstIndex()),o=Math.max(e.index,n),c=(0,r.ensureNotNull)(s.lastIndex()),a=Math.min(c,t.index-1);for(const e of s.rangeIterator(o,a+1)){const t=e.value;if(null===t)continue;const i=this._checkStopPrice(t);if(null!=i)return{index:e.index,price:i}}return null}_changeEntryPoint(e){const t=this.properties().childs(),i=t.stopPrice.value(),s=t.targetPrice.value(),r=1/this.ownerSourceBase(),n=Math.min(i,s)+r,o=Math.max(i,s)-r;e.price=Math.max(n,Math.min(o,this._roundPrice(e.price))),this.setPoints([e,{...this._points[1],price:e.price}]),t.stopPrice.setValue(i),t.targetPrice.setValue(s)}_updateStudySource(){if(!this._metaInfo||this._isDestroyed)return;const e=this._properties.childs().currency.value(),t=this._model.mainSeries(),i="NONE"!==e&&e!==t.currency()&&!this.isSourceHidden();if(!i&&this._studySource?(this._studySource.destroy(),this._studySource=null,this._properties.childs().entryPointCurrencyRate.setValue(1),this._properties.childs().closePointCurrencyRate.setValue(1)):i&&!this._studySource&&(this._studySource=new RiskRewardStudyDataSource(this._model.chartApi(),t.seriesSource()),this._studySource.nonSeriesData().subscribe((e=>{e&&(this._properties.childs().entryPointCurrencyRate.setValue(e.currency_ratio[0]),this._properties.childs().closePointCurrencyRate.setValue(e.currency_ratio[1]??e.currency_ratio[0]))}))),this._studySource&&this._points.length>=2){const i=this._model.timeScale(),r=t.data().bars().firstIndex(),n=t.data().bars().lastIndex();if(null===r||null===n)return;const o=Math.min(n,Math.max(r,this._points[0].index)),c=Math.min(n,Math.max(r,this._points[1].index)),a=i.indexToTimePoint(o),l=i.indexToTimePoint(c);if(null!==a&&null!==l){const t={...this._studySource.inputs(),start_time:1e3*a,end_time:1e3*l,entry_price:this.entryPrice(),target_price:this.profitPrice(),stop_price:this.stopPrice(),account_currency:e};(0,s.default)(t,this._studySource.inputs())||this._studySource.setInputs(t),this._studySource.isStarted()||this._studySource.start()}}}}},76386:(e,t,i)=>{var s;i.d(t,{RiskDisplayMode:()=>s}),function(e){e.Percentage="percents",e.Money="money"}(s||(s={}))},76050:(e,t,i)=>{var s;i.d(t,{RiskRewardPointIndex:()=>s}),function(e){e[e.Entry=0]="Entry",e[e.Close=1]="Close",e[e.ActualEntry=2]="ActualEntry",e[e.ActualClose=3]="ActualClose"}(s||(s={}))}}]); \ No newline at end of file diff --git a/frontend/charting_library/bundles/1737.51511f925000b99093e3.css b/frontend/charting_library/bundles/1737.51511f925000b99093e3.css new file mode 100644 index 0000000..dd5fa77 --- /dev/null +++ b/frontend/charting_library/bundles/1737.51511f925000b99093e3.css @@ -0,0 +1 @@ +.container-zLVm6B4t{align-items:flex-start;border-radius:4px;cursor:default;display:flex;overflow:auto;padding:8px}.container-zLVm6B4t,html.theme-dark .container-zLVm6B4t{background:var(--themed-color-tooltip-wizard-bg,#2962ff)}.content-zLVm6B4t{padding:4px 8px}.arrowHolder-zLVm6B4t{position:absolute}.arrowHolder-zLVm6B4t:after{border:0 solid;box-sizing:border-box;content:"";display:block;height:0;position:absolute;width:0}.arrowHolder-zLVm6B4t:after,html.theme-dark .arrowHolder-zLVm6B4t:after{border-color:var(--themed-color-tooltip-wizard-bg,#2962ff)}.arrowHolder--above-zLVm6B4t:after,.arrowHolder--below-zLVm6B4t:after{border-left:6px solid;border-left-color:var(--themed-color-tooltip-force-transparent,#0000);border-right:6px solid;left:50%;margin-left:-6px}.arrowHolder--above-zLVm6B4t:after,.arrowHolder--below-zLVm6B4t:after,html.theme-dark .arrowHolder--above-zLVm6B4t:after,html.theme-dark .arrowHolder--below-zLVm6B4t:after{border-right-color:var(--themed-color-tooltip-force-transparent,#0000)}html.theme-dark .arrowHolder--above-zLVm6B4t:after,html.theme-dark .arrowHolder--below-zLVm6B4t:after{border-left-color:var(--themed-color-tooltip-force-transparent,#0000)}.arrowHolder--below-zLVm6B4t:after{border-bottom-width:4px;bottom:100%}.arrowHolder--above-zLVm6B4t:after{border-top-width:4px;top:100%}.arrowHolder--after-zLVm6B4t:after,.arrowHolder--before-zLVm6B4t:after{border-bottom:6px solid;border-top:6px solid;border-top-color:var(--themed-color-tooltip-force-transparent,#0000);margin-top:-6px;top:50%}.arrowHolder--after-zLVm6B4t:after,.arrowHolder--before-zLVm6B4t:after,html.theme-dark .arrowHolder--after-zLVm6B4t:after,html.theme-dark .arrowHolder--before-zLVm6B4t:after{border-bottom-color:var(--themed-color-tooltip-force-transparent,#0000)}html.theme-dark .arrowHolder--after-zLVm6B4t:after,html.theme-dark .arrowHolder--before-zLVm6B4t:after{border-top-color:var(--themed-color-tooltip-force-transparent,#0000)}.arrowHolder--before-zLVm6B4t:after{border-right-width:4px;right:100%}.arrowHolder--after-zLVm6B4t:after{border-left-width:4px;left:100%}.arrowHolder--above-fix-zLVm6B4t{bottom:0}.arrowHolder--after-ltr-fix-zLVm6B4t{right:0}.label-zLVm6B4t{display:flex;flex:1 1 auto;margin-right:24px}.closeButton-zLVm6B4t{color:#fff}.container-kfvcmk8t{display:flex;justify-content:center;left:10px;pointer-events:none;position:absolute;right:10px}.centerElement-kfvcmk8t{pointer-events:auto;z-index:1}.text-kfvcmk8t{color:#fff;font-size:14px;line-height:21px;margin-bottom:auto;word-wrap:break-word} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1737.51511f925000b99093e3.rtl.css b/frontend/charting_library/bundles/1737.51511f925000b99093e3.rtl.css new file mode 100644 index 0000000..3fb42b4 --- /dev/null +++ b/frontend/charting_library/bundles/1737.51511f925000b99093e3.rtl.css @@ -0,0 +1 @@ +.container-zLVm6B4t{align-items:flex-start;border-radius:4px;cursor:default;display:flex;overflow:auto;padding:8px}.container-zLVm6B4t,html.theme-dark .container-zLVm6B4t{background:var(--themed-color-tooltip-wizard-bg,#2962ff)}.content-zLVm6B4t{padding:4px 8px}.arrowHolder-zLVm6B4t{position:absolute}.arrowHolder-zLVm6B4t:after{border:0 solid;box-sizing:border-box;content:"";display:block;height:0;position:absolute;width:0}.arrowHolder-zLVm6B4t:after,html.theme-dark .arrowHolder-zLVm6B4t:after{border-color:var(--themed-color-tooltip-wizard-bg,#2962ff)}.arrowHolder--above-zLVm6B4t:after,.arrowHolder--below-zLVm6B4t:after{border-left:6px solid;border-left-color:var(--themed-color-tooltip-force-transparent,#0000);border-right:6px solid;border-right-color:var(--themed-color-tooltip-force-transparent,#0000);left:50%;margin-left:-6px}html.theme-dark .arrowHolder--above-zLVm6B4t:after,html.theme-dark .arrowHolder--below-zLVm6B4t:after{border-left-color:var(--themed-color-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-tooltip-force-transparent,#0000)}.arrowHolder--below-zLVm6B4t:after{border-bottom-width:4px;bottom:100%}.arrowHolder--above-zLVm6B4t:after{border-top-width:4px;top:100%}.arrowHolder--after-zLVm6B4t:after,.arrowHolder--before-zLVm6B4t:after{border-bottom:6px solid;border-top:6px solid;border-top-color:var(--themed-color-tooltip-force-transparent,#0000);margin-top:-6px;top:50%}.arrowHolder--after-zLVm6B4t:after,.arrowHolder--before-zLVm6B4t:after,html.theme-dark .arrowHolder--after-zLVm6B4t:after,html.theme-dark .arrowHolder--before-zLVm6B4t:after{border-bottom-color:var(--themed-color-tooltip-force-transparent,#0000)}html.theme-dark .arrowHolder--after-zLVm6B4t:after,html.theme-dark .arrowHolder--before-zLVm6B4t:after{border-top-color:var(--themed-color-tooltip-force-transparent,#0000)}.arrowHolder--before-zLVm6B4t:after{border-right-width:4px;right:100%}.arrowHolder--after-zLVm6B4t:after{border-left-width:4px;left:100%}.arrowHolder--above-fix-zLVm6B4t{bottom:0}.arrowHolder--before-rtl-fix-zLVm6B4t{left:0}.arrowHolder--after-ltr-fix-zLVm6B4t{right:0}.label-zLVm6B4t{display:flex;flex:1 1 auto;margin-left:24px}.closeButton-zLVm6B4t{color:#fff}.container-kfvcmk8t{display:flex;justify-content:center;left:10px;pointer-events:none;position:absolute;right:10px}.centerElement-kfvcmk8t{pointer-events:auto;z-index:1}.text-kfvcmk8t{color:#fff;font-size:14px;line-height:21px;margin-bottom:auto;word-wrap:break-word} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1833.1e1cad103085069c69fc.css b/frontend/charting_library/bundles/1833.1e1cad103085069c69fc.css new file mode 100644 index 0000000..9623bd4 --- /dev/null +++ b/frontend/charting_library/bundles/1833.1e1cad103085069c69fc.css @@ -0,0 +1 @@ +.titleWrap-e3jFxbHm{align-items:center;display:flex}.ellipsis-e3jFxbHm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hideInput-e3jFxbHm,.hideText-e3jFxbHm{height:0;visibility:hidden}.empty-e3jFxbHm{height:34px;margin-right:-3px;width:34px}.hideEmpty-e3jFxbHm{margin:0;width:0}.editIcon-e3jFxbHm{align-items:center;border-radius:2px;color:var(--themed-color-toolbar-interactive-element-text-normal,#1a1a1a);cursor:default;display:flex;flex-shrink:0;height:34px;justify-content:center;margin-left:5px;width:34px}html.theme-dark .editIcon-e3jFxbHm{color:var(--themed-color-toolbar-interactive-element-text-normal,#dbdbdb)}@media (any-hover:hover){.editIcon-e3jFxbHm:hover{background-color:var(--themed-color-bg-primary-hover,#f2f2f2)}html.theme-dark .editIcon-e3jFxbHm:hover{background-color:var(--themed-color-bg-primary-hover,#303030)}}.scrollable-Ycj0dUGE{flex:1 1 auto;min-height:145px;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}@media (max-height:290px){.scrollable-Ycj0dUGE{min-height:auto}}@supports (-moz-appearance:none){.scrollable-Ycj0dUGE{scrollbar-color:var(--themed-color-scroll-bg,#9c9c9c) #0000;scrollbar-width:thin}html.theme-dark .scrollable-Ycj0dUGE{scrollbar-color:var(--themed-color-scroll-bg,#3d3d3d) #0000}}.scrollable-Ycj0dUGE::-webkit-scrollbar{height:5px;width:5px}.scrollable-Ycj0dUGE::-webkit-scrollbar-thumb{background-clip:content-box;background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#9c9c9c));border:1px solid #0000;border-radius:3px}html.theme-dark .scrollable-Ycj0dUGE::-webkit-scrollbar-thumb{background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#3d3d3d))}.scrollable-Ycj0dUGE::-webkit-scrollbar-track{background-color:initial;border-radius:3px}.scrollable-Ycj0dUGE::-webkit-scrollbar-corner{display:none}.tabs-Ycj0dUGE,.tabs-xNPrJ8dY{--ui-lib-underline-tabs-hor-padding:20px;padding:0 var(--ui-lib-underline-tabs-hor-padding)} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1833.1e1cad103085069c69fc.rtl.css b/frontend/charting_library/bundles/1833.1e1cad103085069c69fc.rtl.css new file mode 100644 index 0000000..7592b7b --- /dev/null +++ b/frontend/charting_library/bundles/1833.1e1cad103085069c69fc.rtl.css @@ -0,0 +1 @@ +.titleWrap-e3jFxbHm{align-items:center;display:flex}.ellipsis-e3jFxbHm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hideInput-e3jFxbHm,.hideText-e3jFxbHm{height:0;visibility:hidden}.empty-e3jFxbHm{height:34px;margin-left:-3px;width:34px}.hideEmpty-e3jFxbHm{margin:0;width:0}.editIcon-e3jFxbHm{align-items:center;border-radius:2px;color:var(--themed-color-toolbar-interactive-element-text-normal,#1a1a1a);cursor:default;display:flex;flex-shrink:0;height:34px;justify-content:center;margin-right:5px;width:34px}html.theme-dark .editIcon-e3jFxbHm{color:var(--themed-color-toolbar-interactive-element-text-normal,#dbdbdb)}@media (any-hover:hover){.editIcon-e3jFxbHm:hover{background-color:var(--themed-color-bg-primary-hover,#f2f2f2)}html.theme-dark .editIcon-e3jFxbHm:hover{background-color:var(--themed-color-bg-primary-hover,#303030)}}.scrollable-Ycj0dUGE{flex:1 1 auto;min-height:145px;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}@media (max-height:290px){.scrollable-Ycj0dUGE{min-height:auto}}@supports (-moz-appearance:none){.scrollable-Ycj0dUGE{scrollbar-color:var(--themed-color-scroll-bg,#9c9c9c) #0000;scrollbar-width:thin}html.theme-dark .scrollable-Ycj0dUGE{scrollbar-color:var(--themed-color-scroll-bg,#3d3d3d) #0000}}.scrollable-Ycj0dUGE::-webkit-scrollbar{height:5px;width:5px}.scrollable-Ycj0dUGE::-webkit-scrollbar-thumb{background-clip:content-box;background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#9c9c9c));border:1px solid #0000;border-radius:3px}html.theme-dark .scrollable-Ycj0dUGE::-webkit-scrollbar-thumb{background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#3d3d3d))}.scrollable-Ycj0dUGE::-webkit-scrollbar-track{background-color:initial;border-radius:3px}.scrollable-Ycj0dUGE::-webkit-scrollbar-corner{display:none}.tabs-Ycj0dUGE,.tabs-xNPrJ8dY{--ui-lib-underline-tabs-hor-padding:20px;padding:0 var(--ui-lib-underline-tabs-hor-padding)} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1906.40ce159ad2a7f4f15d5c.css b/frontend/charting_library/bundles/1906.40ce159ad2a7f4f15d5c.css new file mode 100644 index 0000000..a6827ef --- /dev/null +++ b/frontend/charting_library/bundles/1906.40ce159ad2a7f4f15d5c.css @@ -0,0 +1 @@ +.wrap-HAxAr6QG{align-items:center;display:flex;flex:1 0 auto;flex-direction:column;justify-content:center;padding-bottom:54px;padding-top:24px}@media (max-height:440px) and (orientation:landscape){.wrap-HAxAr6QG{justify-content:flex-start}.image-HAxAr6QG{display:none}}.text-HAxAr6QG{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:18px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:28px;color:var(--themed-color-empty-state-text,#1a1a1a);cursor:default;line-height:var(--ui-lib-typography-line-height)}html.theme-dark .text-HAxAr6QG{color:var(--themed-color-empty-state-text,#dbdbdb)}.item-nuuDM7vP{align-items:center;cursor:default;display:flex;padding-left:8px}.item-nuuDM7vP.big-nuuDM7vP,.item-nuuDM7vP.normal-nuuDM7vP{height:40px}.item-nuuDM7vP.selected-nuuDM7vP{outline:none;overflow:visible;position:relative}.item-nuuDM7vP.selected-nuuDM7vP:focus{outline:none}.item-nuuDM7vP.selected-nuuDM7vP:focus-visible{outline:none}.item-nuuDM7vP.selected-nuuDM7vP:after{border-style:solid;border-width:2px;box-sizing:border-box;content:"";display:none;height:calc(100% - 4px);left:2px;pointer-events:none;position:absolute;top:2px;width:calc(100% - 4px);z-index:1}.item-nuuDM7vP.selected-nuuDM7vP:focus:after{display:block}.item-nuuDM7vP.selected-nuuDM7vP:focus-visible:after{display:block}.item-nuuDM7vP.selected-nuuDM7vP:focus:not(:focus-visible):after{display:none}.item-nuuDM7vP.selected-nuuDM7vP:after{display:block}.contentCell-nuuDM7vP{width:100%}.content-nuuDM7vP{display:flex;max-width:480px;padding-left:8px}.favourite-nuuDM7vP{background-clip:content-box;border:2px solid #0000;border-radius:6px;box-sizing:border-box;height:26px;margin-left:4px;margin-right:-4px;outline:none;overflow:visible;position:relative;width:26px}.favourite-nuuDM7vP:focus{outline:none}.favourite-nuuDM7vP:focus-visible{outline:none}.favourite-nuuDM7vP:after{border-style:solid;border-width:2px;box-sizing:border-box;content:"";display:none;height:calc(100% + 4px);left:-2px;pointer-events:none;position:absolute;top:-2px;width:calc(100% + 4px);z-index:1}.favourite-nuuDM7vP:focus:after{display:block}.favourite-nuuDM7vP:focus-visible:after{display:block}.favourite-nuuDM7vP:focus:not(:focus-visible):after{display:none}.favoriteActionCell-nuuDM7vP{padding:2px}.iconCell-nuuDM7vP{min-width:36px}.icon-nuuDM7vP{margin-left:8px;padding-left:0}.checkboxInput-nuuDM7vP{height:18px;margin-left:8px;padding:5px;width:18px}.label-nuuDM7vP{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;white-space:normal;word-break:break-word;--ui-lib-typography-font-size:14px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:18px;display:-webkit-box;line-height:var(--ui-lib-typography-line-height);overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:18px;max-height:36px}.section-Og4Rg_SK{border-bottom:1px solid var(--themed-color-divider,#ebebeb);margin-top:6px;width:100%}html.theme-dark .section-Og4Rg_SK{border-bottom:1px solid var(--themed-color-divider,#4a4a4a)}.section-Og4Rg_SK:last-child{margin-bottom:5px}.section-Og4Rg_SK:last-child,html.theme-dark .section-Og4Rg_SK:last-child{border-bottom:1px solid var(--themed-color-static-transparent,#0000)}.heading-Og4Rg_SK{box-sizing:border-box;color:var(--themed-color-default-gray,#707070);cursor:default;font-size:11px;height:34px;line-height:16px;padding:8px 20px;text-transform:uppercase}html.theme-dark .heading-Og4Rg_SK{color:var(--themed-color-default-gray,#8c8c8c)}.dialog-UAy2ZKyS{height:680px;width:480px}@media (max-width:480px){.dialog-UAy2ZKyS{width:100%}}.wrap-UAy2ZKyS{display:flex;flex-flow:column;height:100%;overflow-y:auto}@media (pointer:fine){@supports (-moz-appearance:none){.wrap-UAy2ZKyS{scrollbar-color:var(--themed-color-scroll-bg,#9c9c9c) #0000;scrollbar-width:thin}html.theme-dark .wrap-UAy2ZKyS{scrollbar-color:var(--themed-color-scroll-bg,#3d3d3d) #0000}}.wrap-UAy2ZKyS::-webkit-scrollbar{height:5px;width:5px}.wrap-UAy2ZKyS::-webkit-scrollbar-thumb{background-clip:content-box;background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#9c9c9c));border:1px solid #0000;border-radius:3px}html.theme-dark .wrap-UAy2ZKyS::-webkit-scrollbar-thumb{background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#3d3d3d))}.wrap-UAy2ZKyS::-webkit-scrollbar-track{background-color:initial;border-radius:3px}.wrap-UAy2ZKyS::-webkit-scrollbar-corner{display:none}}.empty-UAy2ZKyS{align-items:center;box-sizing:border-box;color:var(--themed-color-empty-state-text,#1a1a1a);display:flex;flex-flow:column;font-size:16px;height:100%;justify-content:center;line-height:24px;padding:24px}html.theme-dark .empty-UAy2ZKyS{color:var(--themed-color-empty-state-text,#dbdbdb)}.image-UAy2ZKyS{padding-bottom:8px}.emptyState-UAy2ZKyS{padding:0 20px} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1906.40ce159ad2a7f4f15d5c.rtl.css b/frontend/charting_library/bundles/1906.40ce159ad2a7f4f15d5c.rtl.css new file mode 100644 index 0000000..8df1d62 --- /dev/null +++ b/frontend/charting_library/bundles/1906.40ce159ad2a7f4f15d5c.rtl.css @@ -0,0 +1 @@ +.wrap-HAxAr6QG{align-items:center;display:flex;flex:1 0 auto;flex-direction:column;justify-content:center;padding-bottom:54px;padding-top:24px}@media (max-height:440px) and (orientation:landscape){.wrap-HAxAr6QG{justify-content:flex-start}.image-HAxAr6QG{display:none}}.text-HAxAr6QG{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:18px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:28px;color:var(--themed-color-empty-state-text,#1a1a1a);cursor:default;line-height:var(--ui-lib-typography-line-height)}html.theme-dark .text-HAxAr6QG{color:var(--themed-color-empty-state-text,#dbdbdb)}.item-nuuDM7vP{align-items:center;cursor:default;display:flex;padding-right:8px}.item-nuuDM7vP.big-nuuDM7vP,.item-nuuDM7vP.normal-nuuDM7vP{height:40px}.item-nuuDM7vP.selected-nuuDM7vP{outline:none;overflow:visible;position:relative}.item-nuuDM7vP.selected-nuuDM7vP:focus{outline:none}.item-nuuDM7vP.selected-nuuDM7vP:focus-visible{outline:none}.item-nuuDM7vP.selected-nuuDM7vP:after{border-style:solid;border-width:2px;box-sizing:border-box;content:"";display:none;height:calc(100% - 4px);pointer-events:none;position:absolute;right:2px;top:2px;width:calc(100% - 4px);z-index:1}.item-nuuDM7vP.selected-nuuDM7vP:focus:after{display:block}.item-nuuDM7vP.selected-nuuDM7vP:focus-visible:after{display:block}.item-nuuDM7vP.selected-nuuDM7vP:focus:not(:focus-visible):after{display:none}.item-nuuDM7vP.selected-nuuDM7vP:after{display:block}.contentCell-nuuDM7vP{width:100%}.content-nuuDM7vP{display:flex;max-width:480px;padding-right:8px}.favourite-nuuDM7vP{background-clip:content-box;border:2px solid #0000;border-radius:6px;box-sizing:border-box;height:26px;margin-left:-4px;margin-right:4px;outline:none;overflow:visible;position:relative;width:26px}.favourite-nuuDM7vP:focus{outline:none}.favourite-nuuDM7vP:focus-visible{outline:none}.favourite-nuuDM7vP:after{border-style:solid;border-width:2px;box-sizing:border-box;content:"";display:none;height:calc(100% + 4px);pointer-events:none;position:absolute;right:-2px;top:-2px;width:calc(100% + 4px);z-index:1}.favourite-nuuDM7vP:focus:after{display:block}.favourite-nuuDM7vP:focus-visible:after{display:block}.favourite-nuuDM7vP:focus:not(:focus-visible):after{display:none}.favoriteActionCell-nuuDM7vP{padding:2px}.iconCell-nuuDM7vP{min-width:36px}.icon-nuuDM7vP{margin-right:8px;padding-right:0}.checkboxInput-nuuDM7vP{height:18px;margin-right:8px;padding:5px;width:18px}.label-nuuDM7vP{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;white-space:normal;word-break:break-word;--ui-lib-typography-font-size:14px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:18px;display:-webkit-box;line-height:var(--ui-lib-typography-line-height);overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:18px;max-height:36px}.section-Og4Rg_SK{border-bottom:1px solid var(--themed-color-divider,#ebebeb);margin-top:6px;width:100%}html.theme-dark .section-Og4Rg_SK{border-bottom:1px solid var(--themed-color-divider,#4a4a4a)}.section-Og4Rg_SK:last-child{margin-bottom:5px}.section-Og4Rg_SK:last-child,html.theme-dark .section-Og4Rg_SK:last-child{border-bottom:1px solid var(--themed-color-static-transparent,#0000)}.heading-Og4Rg_SK{box-sizing:border-box;color:var(--themed-color-default-gray,#707070);cursor:default;font-size:11px;height:34px;line-height:16px;padding:8px 20px;text-transform:uppercase}html.theme-dark .heading-Og4Rg_SK{color:var(--themed-color-default-gray,#8c8c8c)}.dialog-UAy2ZKyS{height:680px;width:480px}@media (max-width:480px){.dialog-UAy2ZKyS{width:100%}}.wrap-UAy2ZKyS{display:flex;flex-flow:column;height:100%;overflow-y:auto}@media (pointer:fine){@supports (-moz-appearance:none){.wrap-UAy2ZKyS{scrollbar-color:var(--themed-color-scroll-bg,#9c9c9c) #0000;scrollbar-width:thin}html.theme-dark .wrap-UAy2ZKyS{scrollbar-color:var(--themed-color-scroll-bg,#3d3d3d) #0000}}.wrap-UAy2ZKyS::-webkit-scrollbar{height:5px;width:5px}.wrap-UAy2ZKyS::-webkit-scrollbar-thumb{background-clip:content-box;background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#9c9c9c));border:1px solid #0000;border-radius:3px}html.theme-dark .wrap-UAy2ZKyS::-webkit-scrollbar-thumb{background-color:var(--tv-color-scrollbar-thumb-background,var(--themed-color-scroll-bg,#3d3d3d))}.wrap-UAy2ZKyS::-webkit-scrollbar-track{background-color:initial;border-radius:3px}.wrap-UAy2ZKyS::-webkit-scrollbar-corner{display:none}}.empty-UAy2ZKyS{align-items:center;box-sizing:border-box;color:var(--themed-color-empty-state-text,#1a1a1a);display:flex;flex-flow:column;font-size:16px;height:100%;justify-content:center;line-height:24px;padding:24px}html.theme-dark .empty-UAy2ZKyS{color:var(--themed-color-empty-state-text,#dbdbdb)}.image-UAy2ZKyS{padding-bottom:8px}.emptyState-UAy2ZKyS{padding:0 20px} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1933.a40665ade21837a1b5e2.css b/frontend/charting_library/bundles/1933.a40665ade21837a1b5e2.css new file mode 100644 index 0000000..c40b269 --- /dev/null +++ b/frontend/charting_library/bundles/1933.a40665ade21837a1b5e2.css @@ -0,0 +1 @@ +.lollipopTooltipTitle-hkWvPxQc{align-items:center;column-gap:8px;display:flex;justify-content:flex-start;margin-bottom:12px;padding:4px 0}.lollipopTooltipTitle_minimal-hkWvPxQc .lollipopTooltipTitle__title-hkWvPxQc{font-size:16px;line-height:22px}.lollipopTooltipTitle_mobile-hkWvPxQc{padding:0}.lollipopTooltipTitle_mobile-hkWvPxQc .lollipopTooltipTitle__title-hkWvPxQc{font-size:20px;line-height:24px}.lollipopTooltipTitle__icon-hkWvPxQc{color:currentColor;display:flex}.lollipopTooltipTitle__title-hkWvPxQc{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:18px;font-size:var(--ui-lib-typography-font-size);font-weight:600;--ui-lib-typography-line-height:24px;line-height:var(--ui-lib-typography-line-height)}.wrap-tm3FiOQl{background:var(--tv-color-popup-background,var(--themed-color-tooltip-background,#fff))}html.theme-dark .wrap-tm3FiOQl{background:var(--tv-color-popup-background,var(--themed-color-tooltip-background,#262626))}.content-tm3FiOQl a,.content-tm3FiOQl span{cursor:default}.content-tm3FiOQl .subtitle-tm3FiOQl{font-size:14px;line-height:21px}.content-tm3FiOQl .subtitle-tm3FiOQl .text-tm3FiOQl{flex:1 0 0;overflow:hidden;text-overflow:ellipsis}.content-tm3FiOQl .group-tm3FiOQl{padding-top:12px}.content-tm3FiOQl .groupIcon-tm3FiOQl{border-radius:9px;display:inline-block;height:18px;margin-left:7px;vertical-align:top;width:18px}.content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl{background-color:var(--themed-color-overlay-warning-1-light,#ff980026);color:var(--themed-color-before-market-open,#fb8c00)}html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl{background-color:var(--themed-color-overlay-warning-1-light,#ff980033);color:var(--themed-color-before-market-open,#fb8c00)}@media (any-hover:hover){.content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl:hover,html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl:hover{background-color:var(--themed-color-overlay-warning-1-normal,#ff98004d)}}.content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl{background-color:var(--themed-color-overlay-accent-1-light,#2962ff26);color:var(--themed-color-after-market-close,#2962ff)}html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl{background-color:var(--themed-color-overlay-accent-1-light,#448aff33);color:var(--themed-color-after-market-close,#2962ff)}@media (any-hover:hover){.content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl:hover{background-color:var(--themed-color-overlay-accent-1-normal,#2962ff4d)}html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl:hover{background-color:var(--themed-color-overlay-accent-1-normal,#448aff4d)}}.content-tm3FiOQl .groupTitle-tm3FiOQl{color:var(--themed-color-default-gray,#707070);font-size:11px;font-weight:400;letter-spacing:.4px;line-height:16px;text-transform:uppercase}html.theme-dark .content-tm3FiOQl .groupTitle-tm3FiOQl{color:var(--themed-color-default-gray,#8c8c8c)}.content-tm3FiOQl .groupRow-tm3FiOQl{display:flex;flex-direction:row}.content-tm3FiOQl .groupCell-tm3FiOQl{flex:1 0 0}.content-tm3FiOQl .group-tm3FiOQl .text-tm3FiOQl{font-size:14px;line-height:22px;overflow:hidden;text-overflow:ellipsis}.content-tm3FiOQl.mob-tm3FiOQl .group-tm3FiOQl .text-tm3FiOQl,.content-tm3FiOQl.mob-tm3FiOQl .subtitle-tm3FiOQl{font-size:16px;line-height:24px}.content-tm3FiOQl.mini-tm3FiOQl .subtitle-tm3FiOQl{font-size:13px;line-height:19px}.generalContent-tm3FiOQl>div{background:var(--themed-color-content-item-bg,#fff)}html.theme-dark .generalContent-tm3FiOQl>div{background:var(--themed-color-content-item-bg,#262626)}@media (any-hover:hover){.generalContent-tm3FiOQl>div:hover{background-color:var(--themed-color-bg-primary-hover,#f2f2f2);cursor:pointer}html.theme-dark .generalContent-tm3FiOQl>div:hover{background-color:var(--themed-color-bg-primary-hover,#303030)}}.keyFactContent-tm3FiOQl{background:linear-gradient(277deg,#fff0 70.02%,#d500f91a 100.21%),linear-gradient(263deg,#fff0 69.93%,#00bce51a),linear-gradient(79deg,#fff0 65.84%,#d500f91a),linear-gradient(101deg,#fff0 65.9%,#00bce51a 100.13%);font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:12px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:16px;border-radius:8px;line-height:var(--ui-lib-typography-line-height);margin-bottom:12px;padding:4px 6px}.keyFactContent__title-tm3FiOQl{padding-left:22px;position:relative}.keyFactContent__title-tm3FiOQl:before{background-color:#7c4dff;content:" ";height:14px;left:0;-webkit-mask-image:url(sparkle.52cc9730ec8e15c42aa9.svg);mask-image:url(sparkle.52cc9730ec8e15c42aa9.svg);position:absolute;top:-2px;width:18px}.keyFactContent__title-tm3FiOQl:after{color:var(--themed-color-content-primary-neutral,#707070);content:"・"}html.theme-dark .keyFactContent__title-tm3FiOQl:after{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.newsContentItem-tm3FiOQl{margin-left:-12px;margin-right:-16px;padding-left:12px;padding-right:16px}.drawer-xBKhVqal{padding:0}.drawer-xBKhVqal>:not(:last-child){border-bottom:1px solid var(--themed-color-cold-gray-150,#ebebeb)}.drawerItem-xBKhVqal{color:var(--themed-color-tooltip-text,#1a1a1a);padding:16px}html.theme-dark .drawerItem-xBKhVqal{color:var(--themed-color-tooltip-text,#dbdbdb)}.menuWrap-xBKhVqal{background:var(--themed-color-menu-wrapper,#0000)}.menuWrap-xBKhVqal,html.theme-dark .menuWrap-xBKhVqal{box-shadow:0 0 var(--themed-color-menu-wrapper,#0000)}html.theme-dark .menuWrap-xBKhVqal{background:var(--themed-color-menu-wrapper,#0000)}.menuWrap-xBKhVqal .scrollWrap-xBKhVqal{overflow-y:hidden!important}.menuWrap-xBKhVqal .menuBox-xBKhVqal{margin:2px 4px 4px;padding:0}.card-xBKhVqal{border-left:4px solid;border-radius:4px;box-shadow:0 2px 4px 0 var(--themed-color-other-shadow-primary-neutral-extra-heavy,#0003);box-sizing:border-box;color:var(--themed-color-tooltip-text,#1a1a1a);padding:16px 16px 16px 12px;width:300px}html.theme-dark .card-xBKhVqal{box-shadow:0 2px 4px 0 var(--themed-color-other-shadow-primary-neutral-extra-heavy,#0006);color:var(--themed-color-tooltip-text,#dbdbdb)}.card-xBKhVqal:not(:first-child){margin-top:8px}.fadeTop-xBKhVqal{background:linear-gradient(180deg,#fff,#fff0);height:10px;position:absolute;top:0}html.theme-dark .fadeTop-xBKhVqal{background:linear-gradient(180deg,#1a1a1a,#1a1a1a00)}.fadeBottom-xBKhVqal{background:linear-gradient(0deg,#fff,#fff0);bottom:0;height:10px;position:absolute}html.theme-dark .fadeBottom-xBKhVqal{background:linear-gradient(0deg,#1a1a1a,#1a1a1a00)} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1933.a40665ade21837a1b5e2.rtl.css b/frontend/charting_library/bundles/1933.a40665ade21837a1b5e2.rtl.css new file mode 100644 index 0000000..7158fca --- /dev/null +++ b/frontend/charting_library/bundles/1933.a40665ade21837a1b5e2.rtl.css @@ -0,0 +1 @@ +.lollipopTooltipTitle-hkWvPxQc{align-items:center;column-gap:8px;display:flex;justify-content:flex-start;margin-bottom:12px;padding:4px 0}.lollipopTooltipTitle_minimal-hkWvPxQc .lollipopTooltipTitle__title-hkWvPxQc{font-size:16px;line-height:22px}.lollipopTooltipTitle_mobile-hkWvPxQc{padding:0}.lollipopTooltipTitle_mobile-hkWvPxQc .lollipopTooltipTitle__title-hkWvPxQc{font-size:20px;line-height:24px}.lollipopTooltipTitle__icon-hkWvPxQc{color:currentColor;display:flex}.lollipopTooltipTitle__title-hkWvPxQc{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:18px;font-size:var(--ui-lib-typography-font-size);font-weight:600;--ui-lib-typography-line-height:24px;line-height:var(--ui-lib-typography-line-height)}.wrap-tm3FiOQl{background:var(--tv-color-popup-background,var(--themed-color-tooltip-background,#fff))}html.theme-dark .wrap-tm3FiOQl{background:var(--tv-color-popup-background,var(--themed-color-tooltip-background,#262626))}.content-tm3FiOQl a,.content-tm3FiOQl span{cursor:default}.content-tm3FiOQl .subtitle-tm3FiOQl{font-size:14px;line-height:21px}.content-tm3FiOQl .subtitle-tm3FiOQl .text-tm3FiOQl{flex:1 0 0;overflow:hidden;text-overflow:ellipsis}.content-tm3FiOQl .group-tm3FiOQl{padding-top:12px}.content-tm3FiOQl .groupIcon-tm3FiOQl{border-radius:9px;display:inline-block;height:18px;margin-right:7px;vertical-align:top;width:18px}.content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl{background-color:var(--themed-color-overlay-warning-1-light,#ff980026);color:var(--themed-color-before-market-open,#fb8c00)}html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl{background-color:var(--themed-color-overlay-warning-1-light,#ff980033);color:var(--themed-color-before-market-open,#fb8c00)}@media (any-hover:hover){.content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl:hover,html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.beforeMarketOpen-tm3FiOQl:hover{background-color:var(--themed-color-overlay-warning-1-normal,#ff98004d)}}.content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl{background-color:var(--themed-color-overlay-accent-1-light,#2962ff26);color:var(--themed-color-after-market-close,#2962ff)}html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl{background-color:var(--themed-color-overlay-accent-1-light,#448aff33);color:var(--themed-color-after-market-close,#2962ff)}@media (any-hover:hover){.content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl:hover{background-color:var(--themed-color-overlay-accent-1-normal,#2962ff4d)}html.theme-dark .content-tm3FiOQl .groupIcon-tm3FiOQl.afterMarketClose-tm3FiOQl:hover{background-color:var(--themed-color-overlay-accent-1-normal,#448aff4d)}}.content-tm3FiOQl .groupTitle-tm3FiOQl{color:var(--themed-color-default-gray,#707070);font-size:11px;font-weight:400;letter-spacing:.4px;line-height:16px;text-transform:uppercase}html.theme-dark .content-tm3FiOQl .groupTitle-tm3FiOQl{color:var(--themed-color-default-gray,#8c8c8c)}.content-tm3FiOQl .groupRow-tm3FiOQl{display:flex;flex-direction:row}.content-tm3FiOQl .groupCell-tm3FiOQl{flex:1 0 0}.content-tm3FiOQl .group-tm3FiOQl .text-tm3FiOQl{font-size:14px;line-height:22px;overflow:hidden;text-overflow:ellipsis}.content-tm3FiOQl.mob-tm3FiOQl .group-tm3FiOQl .text-tm3FiOQl,.content-tm3FiOQl.mob-tm3FiOQl .subtitle-tm3FiOQl{font-size:16px;line-height:24px}.content-tm3FiOQl.mini-tm3FiOQl .subtitle-tm3FiOQl{font-size:13px;line-height:19px}.generalContent-tm3FiOQl>div{background:var(--themed-color-content-item-bg,#fff)}html.theme-dark .generalContent-tm3FiOQl>div{background:var(--themed-color-content-item-bg,#262626)}@media (any-hover:hover){.generalContent-tm3FiOQl>div:hover{background-color:var(--themed-color-bg-primary-hover,#f2f2f2);cursor:pointer}html.theme-dark .generalContent-tm3FiOQl>div:hover{background-color:var(--themed-color-bg-primary-hover,#303030)}}.keyFactContent-tm3FiOQl{background:linear-gradient(-277deg,#fff0 70.02%,#d500f91a 100.21%),linear-gradient(-263deg,#fff0 69.93%,#00bce51a),linear-gradient(-79deg,#fff0 65.84%,#d500f91a),linear-gradient(-101deg,#fff0 65.9%,#00bce51a 100.13%);font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:12px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:16px;border-radius:8px;line-height:var(--ui-lib-typography-line-height);margin-bottom:12px;padding:4px 6px}.keyFactContent__title-tm3FiOQl{padding-right:22px;position:relative}.keyFactContent__title-tm3FiOQl:before{background-color:#7c4dff;content:" ";height:14px;-webkit-mask-image:url(sparkle.52cc9730ec8e15c42aa9.svg);mask-image:url(sparkle.52cc9730ec8e15c42aa9.svg);position:absolute;right:0;top:-2px;width:18px}.keyFactContent__title-tm3FiOQl:after{color:var(--themed-color-content-primary-neutral,#707070);content:"・"}html.theme-dark .keyFactContent__title-tm3FiOQl:after{color:var(--themed-color-content-primary-neutral,#8c8c8c)}.newsContentItem-tm3FiOQl{margin-left:-16px;margin-right:-12px;padding-left:16px;padding-right:12px}.drawer-xBKhVqal{padding:0}.drawer-xBKhVqal>:not(:last-child){border-bottom:1px solid var(--themed-color-cold-gray-150,#ebebeb)}.drawerItem-xBKhVqal{color:var(--themed-color-tooltip-text,#1a1a1a);padding:16px}html.theme-dark .drawerItem-xBKhVqal{color:var(--themed-color-tooltip-text,#dbdbdb)}.menuWrap-xBKhVqal{background:var(--themed-color-menu-wrapper,#0000)}.menuWrap-xBKhVqal,html.theme-dark .menuWrap-xBKhVqal{box-shadow:0 0 var(--themed-color-menu-wrapper,#0000)}html.theme-dark .menuWrap-xBKhVqal{background:var(--themed-color-menu-wrapper,#0000)}.menuWrap-xBKhVqal .scrollWrap-xBKhVqal{overflow-y:hidden!important}.menuWrap-xBKhVqal .menuBox-xBKhVqal{margin:2px 4px 4px;padding:0}.card-xBKhVqal{border-radius:4px;border-right:4px solid;box-shadow:0 2px 4px 0 var(--themed-color-other-shadow-primary-neutral-extra-heavy,#0003);box-sizing:border-box;color:var(--themed-color-tooltip-text,#1a1a1a);padding:16px 12px 16px 16px;width:300px}html.theme-dark .card-xBKhVqal{box-shadow:0 2px 4px 0 var(--themed-color-other-shadow-primary-neutral-extra-heavy,#0006);color:var(--themed-color-tooltip-text,#dbdbdb)}.card-xBKhVqal:not(:first-child){margin-top:8px}.fadeTop-xBKhVqal{background:linear-gradient(-180deg,#fff,#fff0);height:10px;position:absolute;top:0}html.theme-dark .fadeTop-xBKhVqal{background:linear-gradient(-180deg,#1a1a1a,#1a1a1a00)}.fadeBottom-xBKhVqal{background:linear-gradient(0deg,#fff,#fff0);bottom:0;height:10px;position:absolute}html.theme-dark .fadeBottom-xBKhVqal{background:linear-gradient(0deg,#1a1a1a,#1a1a1a00)} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1996.25e6f30e7a095ec239f4.css b/frontend/charting_library/bundles/1996.25e6f30e7a095ec239f4.css new file mode 100644 index 0000000..e8497e7 --- /dev/null +++ b/frontend/charting_library/bundles/1996.25e6f30e7a095ec239f4.css @@ -0,0 +1 @@ +html{-webkit-text-size-adjust:100%}body{color:var(--themed-color-text-primary,#1a1a1a);font-size:14px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on}html.theme-dark body{color:var(--themed-color-text-primary,#dbdbdb)}html[lang=vi] body{font-family:Arial,sans-serif}body,html{box-sizing:border-box}body,dir,h1,h2,h3,h4,h5,h6,html,li,menu,ol,p,ul{margin:0;padding:0}a{text-decoration:none}a:active{outline:0}@media (any-hover:hover){a:hover{outline:0}}h1{font-size:2em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sup{top:-.25em}sub{bottom:-.25em}figure{margin:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button,select{text-transform:none}button,input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:default}input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:initial}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:none;margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}:root{--v-rhythm-header-1-space-phone:56px;--v-rhythm-header-1-space-tablet:80px;--v-rhythm-header-1-space-laptop:100px;--v-rhythm-header-1-space-desktop:120px;--v-rhythm-header-2-space-phone:16px;--v-rhythm-header-2-space-tablet:24px;--v-rhythm-header-2-space-laptop:32px;--v-rhythm-header-2-space-desktop:40px;--v-rhythm-header-3-space-phone:16px;--v-rhythm-header-3-space-tablet:16px;--v-rhythm-header-3-space-laptop:24px;--v-rhythm-header-3-space-desktop:24px;--v-rhythm-footer-1-space-phone:56px;--v-rhythm-footer-1-space-tablet:80px;--v-rhythm-footer-1-space-laptop:100px;--v-rhythm-footer-1-space-desktop:120px;--v-rhythm-footer-2-space-phone:16px;--v-rhythm-footer-2-space-tablet:24px;--v-rhythm-footer-2-space-laptop:32px;--v-rhythm-footer-2-space-desktop:40px;--v-rhythm-footer-3-space-phone:16px;--v-rhythm-footer-3-space-tablet:16px;--v-rhythm-footer-3-space-laptop:24px;--v-rhythm-footer-3-space-desktop:24px;--v-rhythm-spacing-1-phone:80px;--v-rhythm-spacing-1-tablet:120px;--v-rhythm-spacing-1-laptop:160px;--v-rhythm-spacing-1-desktop:200px;--v-rhythm-spacing-2-phone:64px;--v-rhythm-spacing-2-tablet:88px;--v-rhythm-spacing-2-laptop:120px;--v-rhythm-spacing-2-desktop:160px;--v-rhythm-spacing-3-phone:48px;--v-rhythm-spacing-3-tablet:64px;--v-rhythm-spacing-3-laptop:88px;--v-rhythm-spacing-3-desktop:120px;--v-rhythm-spacing-4-phone:48px;--v-rhythm-spacing-4-tablet:48px;--v-rhythm-spacing-4-laptop:64px;--v-rhythm-spacing-4-desktop:80px;--v-rhythm-spacing-5-phone:32px;--v-rhythm-spacing-5-tablet:32px;--v-rhythm-spacing-5-laptop:40px;--v-rhythm-spacing-5-desktop:48px;--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-phone);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-phone);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-phone);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-phone);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-phone);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-phone);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-phone);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-phone);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-phone);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-phone);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-phone)}@media (min-width:768px){:root{--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-tablet);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-tablet);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-tablet);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-tablet);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-tablet);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-tablet);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-tablet);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-tablet);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-tablet);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-tablet);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-tablet)}}@media (min-width:1280px){:root{--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-laptop);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-laptop);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-laptop);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-laptop);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-laptop);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-laptop);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-laptop);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-laptop);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-laptop);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-laptop);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-laptop)}}@media (min-width:1440px){:root{--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-desktop);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-desktop);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-desktop);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-desktop);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-desktop);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-desktop);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-desktop);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-desktop);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-desktop);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-desktop);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-desktop)}}.tv-text ol,.tv-text p,.tv-text ul{color:var(--themed-color-text-primary,#1a1a1a);font-size:14px;line-height:1.6;margin-bottom:12px}html.theme-dark .tv-text ol,html.theme-dark .tv-text p,html.theme-dark .tv-text ul{color:var(--themed-color-text-primary,#dbdbdb)}.tv-text ol.tv-text__paragraph--additional-top-margin,.tv-text p.tv-text__paragraph--additional-top-margin,.tv-text ul.tv-text__paragraph--additional-top-margin{margin-top:24px}.tv-text ol.tv-text__paragraph--additional-top-margin_double,.tv-text p.tv-text__paragraph--additional-top-margin_double,.tv-text ul.tv-text__paragraph--additional-top-margin_double{margin-top:48px}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link,.tv-text p.tv-text__paragraph--additional-top-margin_double.link,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:visited,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:visited,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:visited{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link:visited,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link:visited,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link:visited{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:hover{color:var(--themed-color-link-primary-hover,#1e53e5);fill:var(--themed-color-link-primary-hover,#1e53e5)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link:hover,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link:hover,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link:hover{fill:var(--themed-color-link-primary-hover,#3179f5);color:var(--themed-color-link-primary-hover,#3179f5)}}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:active,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:active,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:active{color:var(--themed-color-link-primary-active,#1848cc);fill:var(--themed-color-link-primary-active,#1848cc)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link:active,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link:active,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link:active{fill:var(--themed-color-link-primary-active,#2962ff);color:var(--themed-color-link-primary-active,#2962ff)}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:focus,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:focus,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:focus{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:focus-visible,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:focus-visible,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:focus-visible{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:focus:not(:focus-visible),.tv-text p.tv-text__paragraph--additional-top-margin_double.link:focus:not(:focus-visible),.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:focus:not(:focus-visible){outline:none}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:hover{text-decoration:underline}}.tv-text ol.tv-text__paragraph--additional-bottom-margin,.tv-text p.tv-text__paragraph--additional-bottom-margin,.tv-text ul.tv-text__paragraph--additional-bottom-margin{margin-bottom:24px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double,.tv-text p.tv-text__paragraph--additional-bottom-margin_double,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double{margin-bottom:48px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:visited,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:visited,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:visited{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:visited,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:visited,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:visited{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:hover{color:var(--themed-color-link-primary-hover,#1e53e5);fill:var(--themed-color-link-primary-hover,#1e53e5)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:hover,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:hover,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:hover{fill:var(--themed-color-link-primary-hover,#3179f5);color:var(--themed-color-link-primary-hover,#3179f5)}}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:active,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:active,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:active{color:var(--themed-color-link-primary-active,#1848cc);fill:var(--themed-color-link-primary-active,#1848cc)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:active,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:active,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:active{fill:var(--themed-color-link-primary-active,#2962ff);color:var(--themed-color-link-primary-active,#2962ff)}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:focus,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:focus,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:focus{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:focus-visible,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:focus-visible,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:focus-visible{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:focus:not(:focus-visible),.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:focus:not(:focus-visible),.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:focus:not(:focus-visible){outline:none}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:hover{text-decoration:underline}}.tv-text h1{font-size:45px;margin-bottom:30px;margin-top:30px}@media (max-width:1019px){.tv-text h1{font-size:38px}}@media (max-width:767px){.tv-text h1{font-size:32px}}@media (max-width:479px){.tv-text h1{font-size:28px}}.tv-text h2{font-size:31px}@media (max-width:1019px){.tv-text h2{font-size:26px}}@media (max-width:479px){.tv-text h2{font-size:24px}}.tv-text h3{font-size:17px}@media (max-width:1019px){.tv-text h3{font-size:16px}}.tv-text h4{font-size:15px}.tv-text h2,.tv-text h3,.tv-text h4{margin-bottom:20px;margin-top:20px}.tv-text h1:first-child,.tv-text h2:first-child,.tv-text h3:first-child,.tv-text h4:first-child{margin-top:0}.tv-text ol,.tv-text ul{list-style-position:inside}.tv-text--position-outside ol,.tv-text--position-outside ul{list-style-position:outside;padding-left:17px}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag){color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag){fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):visited{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):visited{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}@media (any-hover:hover){.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):hover{color:var(--themed-color-link-primary-hover,#1e53e5);fill:var(--themed-color-link-primary-hover,#1e53e5)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):hover{fill:var(--themed-color-link-primary-hover,#3179f5);color:var(--themed-color-link-primary-hover,#3179f5)}}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):active{color:var(--themed-color-link-primary-active,#1848cc);fill:var(--themed-color-link-primary-active,#1848cc)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):active{fill:var(--themed-color-link-primary-active,#2962ff);color:var(--themed-color-link-primary-active,#2962ff)}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):focus{outline:auto;outline-offset:2px}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):focus-visible{outline:auto;outline-offset:2px}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):focus:not(:focus-visible){outline:none}.tv-text__font.tv-text__font--size_semilarge{font-size:15px}@media (max-width:767px){.tv-text__font.tv-text__font--size_semilarge{font-size:14px}}.tv-text__font.tv-text__font--size_large{font-size:17px}@media (max-width:767px){.tv-text__font.tv-text__font--size_large{font-size:16px}}.tv-text__font.tv-text__font--size_xlarge{font-size:19px}@media (max-width:767px){.tv-text__font.tv-text__font--size_xlarge{font-size:18px}}.tv-text__font--bold{font-weight:700}.tv-text__font--italic{font-style:italic}.tv-text--darkbg,.tv-text--darkbg ol,.tv-text--darkbg p,.tv-text--darkbg ul{color:#fff}.js-hidden{display:none!important}.js-no-pointer-events{pointer-events:none!important}.aria-live-regions-wrapper{border:0;height:1px;margin:-1px;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);overflow-x:hidden}html{color-scheme:light}html.theme-dark{color-scheme:dark}html.theme-dark iframe{color-scheme:normal}body{box-sizing:border-box;min-width:320px;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-tap-highlight-color:transparent}body.i-no-scroll{overflow:hidden!important}body.i-no-padding{padding:0!important}::selection{background:var(--themed-color-selection-bg,#bbd9fb)}html.theme-dark ::selection{background:var(--themed-color-selection-bg,#143a87)}.i-hidden{display:none!important}.i-invisible{visibility:hidden!important}.i-clearfix:after{clear:both;content:"";display:table}.i-align_left{text-align:left!important}.i-align_right{text-align:right!important}.i-align_center{text-align:center!important}.i-float_left{float:left!important}.i-float_right{float:right!important}.i-float_none{float:none!important}@media (min-width:1020px){.i-device-only{display:none!important}}@media (max-width:1019px){.i-desktop-only{display:none!important}}@media not all and (max-width:479px){.i-phones-only{display:none!important}}@media (max-width:479px){.i-except-phones-only{display:none!important}}.i-no-break{white-space:nowrap}body{overflow-y:scroll}.tv-layout-width{box-sizing:border-box;margin:0 auto;padding:0 20px;width:1020px}.tv-layout-width--no-padding{padding:0;width:980px}.tv-layout-width--simple{padding:0}.tv-dialog .tv-layout-width{max-width:100%}.chart-page .tv-layout-width{width:100%}@media (max-width:1019px){.tv-layout-width{width:auto}.tv-layout-width .tv-feed{margin-left:-20px;margin-right:-20px}.tv-layout-width .tv-feed--tablet-top-indent{margin-top:48px}}.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{width:auto;--adaptive-mf-container-padding:20px;--adaptive-mf-container-max-width:2360px;box-sizing:initial;padding-left:var(--adaptive-mf-container-padding);padding-right:var(--adaptive-mf-container-padding)}@media (min-width:1024px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:32px}}@media (min-width:1440px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:40px}}@media (min-width:1920px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:100px}}@media (min-width:2560px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:calc((100vw - var(--adaptive-mf-container-max-width))/2);margin-left:auto;margin-right:auto;max-width:var(--adaptive-mf-container-max-width)}}.filter-ideas-button-with-padding{padding-inline:5px}.tv-layout-padding{padding:0 20px}body.page-wide .tv-layout-width{width:1520px}.tv-main,body.page-fullwidth .tv-layout-width{width:100%}.tv-main{display:flex;flex-direction:column;min-height:100%}.clear{clear:both}.clearfix:after{clear:both;content:"";display:table}.hide{display:none!important}.show{display:block!important}.tv-right{float:right!important}.tv-left{float:left!important}body{background-color:var(--themed-color-body-bg,#fff)}html.theme-dark body{background-color:var(--themed-color-body-bg,#000)}body.extension{min-width:0;overflow:hidden}img{border:none}textarea{resize:none}:focus{outline:none}input,textarea{border-radius:0}.tv-profile .tags,.unselectable{-webkit-user-select:none;user-select:none}.selectable,input,textarea{-webkit-user-select:text;user-select:text}.text-center{text-align:center}.loading-indicator{background:var(--themed-color-ui-loading-indicator-bg,#fff);height:100%;left:0;position:absolute;top:0;width:100%;z-index:150}html.theme-dark .loading-indicator{background:var(--themed-color-ui-loading-indicator-bg,#1a1a1a)}.falling,.growing{color:#fff}.growing{background:#42bda8}.falling{background:#f7525f}body,html{background:none;height:100%;overflow:hidden;width:100%}body{min-width:240px}#library-container{background:#fff;border:1px solid #d9dadb}#library-container #showExtendedHoursLink{display:none}.on-widget .open-popup{background:#6798bb;border-width:0!important;border:none;margin-left:4px;padding:7px}@media (any-hover:hover){.on-widget .open-popup:hover{background:#69a3cc}}.on-widget .open-popup:active{background:#71acd6}.on-widget .open-popup svg{display:block;height:14px;width:16px;fill:#fff}.on-widget .widgetbar-widget-hotlist .widgetbar-widgetheader .widgetbar-headerspace{display:none}.text .logo-highlighted{font-weight:700;text-decoration:underline}.on-cme-widget .symbol-edit-popup .filter,.on-cme-widget .symbol-search-dialog .filter{display:none}.on-cme-widget .symbol-search-dialog .results{height:450px}.load-chart .chart-search,.load-chart .chart-search input{width:100%}@media (max-width:750px){.charts-popup-list .item.save-load-chart-title{display:block}}.charts-popup-list .item .title,.charts-popup-list .item .title-expanded{width:auto}.common-tooltip-EJBD96zX{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:13px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:18px;color:var(--themed-color-common-tooltip-text,#f2f2f2);display:inline-flex;line-height:var(--ui-lib-typography-line-height);opacity:1;pointer-events:none;position:fixed;transition:opacity .15s linear;z-index:1000}.common-tooltip--hidden-EJBD96zX{opacity:0}.common-tooltip--horizontal-EJBD96zX{margin:4px 0}.common-tooltip--horizontal-EJBD96zX.common-tooltip--farther-EJBD96zX{margin:8px 0}.common-tooltip--vertical-EJBD96zX{margin:0 4px}.common-tooltip--vertical-EJBD96zX.common-tooltip-farther-EJBD96zX{margin:0 8px}.common-tooltip--direction_normal-EJBD96zX{flex-direction:row}.common-tooltip--direction_normal-EJBD96zX .common-tooltip__body-EJBD96zX{border-bottom-left-radius:2px;border-top-left-radius:2px}.common-tooltip--direction_normal-EJBD96zX .common-tooltip__body--no-buttons-EJBD96zX,.common-tooltip--direction_normal-EJBD96zX .common-tooltip__button-container-EJBD96zX{border-bottom-right-radius:2px;border-top-right-radius:2px}.common-tooltip--direction_normal-EJBD96zX .common-tooltip__button-EJBD96zX:not(:last-child){margin-right:1px}.common-tooltip--direction_reversed-EJBD96zX{flex-direction:row-reverse}.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__body-EJBD96zX{border-bottom-right-radius:2px;border-top-right-radius:2px}.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__body--no-buttons-EJBD96zX,.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__button-container-EJBD96zX{border-bottom-left-radius:2px;border-top-left-radius:2px}.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__button-EJBD96zX:not(:first-child){margin-left:1px}.common-tooltip__ear-holder-EJBD96zX{position:relative}.common-tooltip__ear-holder-EJBD96zX:after{border:0 solid;border-color:var(--themed-color-common-tooltip-bg,#303030);box-sizing:border-box;content:"";display:block;height:0;position:absolute;width:0}html.theme-dark .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-common-tooltip-bg,#3d3d3d)}.common-tooltip__ear-holder--above-EJBD96zX:after,.common-tooltip__ear-holder--below-EJBD96zX:after{border-left:6px solid;border-left-color:var(--themed-color-tooltip-force-transparent,#0000);border-right:6px solid;border-right-color:var(--themed-color-tooltip-force-transparent,#0000);left:50%;margin-left:-6px}html.theme-dark .common-tooltip__ear-holder--above-EJBD96zX:after,html.theme-dark .common-tooltip__ear-holder--below-EJBD96zX:after{border-left-color:var(--themed-color-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-tooltip-force-transparent,#0000)}.common-tooltip__ear-holder--below-EJBD96zX:after{border-bottom-width:4px;bottom:100%}.common-tooltip__ear-holder--above-EJBD96zX:after{border-top-width:4px;top:100%}.common-tooltip__ear-holder--after-EJBD96zX:after,.common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom:6px solid;border-bottom-color:var(--themed-color-tooltip-force-transparent,#0000);border-top:6px solid;border-top-color:var(--themed-color-tooltip-force-transparent,#0000);margin-top:-6px;top:50%}html.theme-dark .common-tooltip__ear-holder--after-EJBD96zX:after,html.theme-dark .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom-color:var(--themed-color-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-tooltip-force-transparent,#0000)}.common-tooltip__ear-holder--before-EJBD96zX:after{border-right-width:4px;right:100%}.common-tooltip__ear-holder--after-EJBD96zX:after{border-left-width:4px;left:100%}.common-tooltip__body-EJBD96zX{background-color:var(--themed-color-common-tooltip-bg,#303030);box-sizing:border-box;display:block;max-width:310px;padding:3px 8px;position:relative;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;text-align:left}html.theme-dark .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-common-tooltip-bg,#3d3d3d)}.common-tooltip__body--with-hotkey-EJBD96zX{display:flex;max-width:420px;padding:0}.common-tooltip__body--width_wide-EJBD96zX{max-width:640px}.common-tooltip__body--width_narrow-EJBD96zX{max-width:200px}.common-tooltip__body--no-padding-EJBD96zX{padding:0}.common-tooltip__hotkey-block-EJBD96zX{align-items:center;color:#f2f2f2;display:inline-flex;flex:1 0 auto;justify-content:center;line-height:12px;margin:3px 0;padding-right:8px}.common-tooltip__hotkey-block--divider-EJBD96zX:before{background-color:#636363;content:"";height:100%;margin-right:8px;width:1px}.common-tooltip__hotkey-text-EJBD96zX{align-items:center;display:inline-flex;margin:3px 8px}.common-tooltip__hotkey-button-EJBD96zX{background-color:#636363;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;height:18px;padding:1px 6px;width:auto;--ui-lib-typography-line-height:16px;line-height:var(--ui-lib-typography-line-height);--ui-lib-typography-font-size:12px;border-bottom:1px solid #8c8c8c;border-radius:4px;box-sizing:border-box;font-size:var(--ui-lib-typography-font-size);font-style:normal;font-weight:600}.common-tooltip__plus-sign-EJBD96zX{height:15px;line-height:14px;text-align:center;width:13px}.common-tooltip__button-container-EJBD96zX{display:flex;overflow:hidden;position:relative}.common-tooltip__button-EJBD96zX{align-items:center;background-color:#2962ff;color:#fff;display:flex;padding:0 10px}@media (any-hover:hover){.common-tooltip__button-EJBD96zX:hover{background-color:#bbd9fb}}.common-tooltip-EJBD96zX.theme-white{color:var(--themed-color-text-primary,#1a1a1a)}html.theme-dark .common-tooltip-EJBD96zX.theme-white{color:var(--themed-color-text-primary,#dbdbdb)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#fff);border-radius:0}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX{border:1px solid var(--themed-color-border,#ebebeb)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX{border:1px solid var(--themed-color-border,#3d3d3d)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#fff)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:after{border-left:6px solid;border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right:6px solid;border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:after{border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom:6px solid;border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top:6px solid;border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:before{border:0 solid;border-color:var(--themed-color-border,#ebebeb);content:"";display:block;height:0;position:absolute;width:0;z-index:1000}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:before{border-color:var(--themed-color-border,#3d3d3d)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:before,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:before{border-left:7px solid;border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right:7px solid;border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000);left:50%;margin-left:-7px}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:before,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:before{border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:before{border-bottom-width:6px;top:-6px}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:before{border-top-width:6px;bottom:-6px}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:before,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:before{border-bottom:7px solid;border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top:7px solid;border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000);margin-top:-7px;top:50%}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:before,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:before{border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:before{border-right-width:6px;left:-6px}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:before{border-left-width:6px;right:-6px}.common-tooltip-EJBD96zX.theme-round-shadow{box-shadow:0 1px 3px 0 #2a2c394a;color:var(--themed-color-text-primary,#1a1a1a)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow{color:var(--themed-color-text-primary,#dbdbdb)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#fff)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#fff)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--above-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--below-EJBD96zX:after{border-left:6px solid;border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right:6px solid;border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--above-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--below-EJBD96zX:after{border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--after-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom:6px solid;border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top:6px solid;border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--after-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-chart .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#fff);border:1px solid var(--themed-color-divider,#ebebeb);border-radius:16px;box-shadow:0 2px 4px var(--themed-color-shadow-primary-neutral-extra-heavy,#0003);max-width:342px}html.theme-dark .common-tooltip-EJBD96zX.theme-chart .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#262626);border:1px solid var(--themed-color-divider,#4a4a4a);box-shadow:0 2px 4px var(--themed-color-shadow-primary-neutral-extra-heavy,#0006)}.common-tooltip-EJBD96zX.theme-chart .common-tooltip__ear-holder-EJBD96zX:after{content:none}.container-B8mkOfAH{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#ebebeb));box-sizing:border-box;height:100%;width:100%}html.theme-dark .container-B8mkOfAH{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#303030))}.container-B8mkOfAH .inner-B8mkOfAH{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#fff));height:100%;width:100%}html.theme-dark .container-B8mkOfAH .inner-B8mkOfAH{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#1a1a1a))}.container-B8mkOfAH.border-left-B8mkOfAH{padding-left:4px}.container-B8mkOfAH.border-right-B8mkOfAH{padding-right:4px}.container-B8mkOfAH.border-top-B8mkOfAH{padding-top:4px}.container-B8mkOfAH.border-bottom-B8mkOfAH{padding-bottom:4px}.container-B8mkOfAH.top-right-radius-B8mkOfAH .inner-B8mkOfAH{border-top-right-radius:0}.container-B8mkOfAH.top-left-radius-B8mkOfAH .inner-B8mkOfAH{border-top-left-radius:0}.container-B8mkOfAH.bottom-right-radius-B8mkOfAH .inner-B8mkOfAH{border-bottom-right-radius:0}.container-B8mkOfAH.bottom-left-radius-B8mkOfAH .inner-B8mkOfAH{border-bottom-left-radius:0}.chart-controls-bar{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#fff));border-top:1px solid;border-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#ebebeb));border-radius:0 0 4px 4px;box-sizing:border-box;contain:strict;height:39px;overflow:hidden;position:absolute}html.theme-dark .chart-controls-bar{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#1a1a1a));border-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#303030))}.no-border-bottom-left-radius .chart-controls-bar{border-bottom-left-radius:0}.no-border-bottom-right-radius .chart-controls-bar{border-bottom-right-radius:0}.tv-spinner{animation:tv-spinner__container-rotate-aLqboHuu .9s linear infinite;border-bottom-color:var(--tv-spinner-color,#2962ff);border-bottom:0 solid var(--themed-color-spinner-bg,#f2f2f2);border-left:0 solid var(--tv-spinner-color,#2962ff);border-radius:50%;border-right-color:var(--tv-spinner-color,#2962ff);border-right:0 solid var(--themed-color-spinner-bg,#f2f2f2);border-top:0 solid var(--tv-spinner-color,#2962ff);display:none;margin:0 auto;position:absolute}html.theme-dark .tv-spinner{border-bottom-color:var(--themed-color-spinner-bg,#303030);border-right-color:var(--themed-color-spinner-bg,#303030)}.tv-spinner--shown{display:block}.tv-spinner--size_xxsmall{border-width:2px;height:10px;left:calc(50% - 7px);top:calc(50% - 7px);width:10px}.tv-spinner--size_xsmall{border-width:2px;height:14px;left:calc(50% - 9px);top:calc(50% - 9px);width:14px}.tv-spinner--size_small{border-width:2px;height:20px;left:calc(50% - 12px);top:calc(50% - 12px);width:20px}.tv-spinner--size_medium{border-width:3px;height:28px;left:calc(50% - 17px);top:calc(50% - 17px);width:28px}.tv-spinner--size_large{border-width:4px;height:56px;left:calc(50% - 32px);top:calc(50% - 32px);width:56px}@keyframes tv-spinner__container-rotate-aLqboHuu{to{transform:rotate(1turn)}}.screen-otjoFNF2{bottom:0;display:none;left:0;opacity:.5;position:absolute;right:0;top:0;z-index:8}.screen-otjoFNF2.fade-otjoFNF2{animation:screenfade-otjoFNF2 .3s ease backwards;display:block}@keyframes screenfade-otjoFNF2{0%{opacity:0}}.paneSeparator-uqBaC1Ki{margin:0;padding:0;position:relative}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki{height:7px;left:0;position:absolute;top:-3px;width:100%;z-index:50}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.mobile-uqBaC1Ki{height:11px;top:-5px}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki,.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.hovered-uqBaC1Ki,.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.selected-uqBaC1Ki{background:var(--themed-color-overlay-accent-1-light,#2962ff26);cursor:row-resize}html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki,html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.hovered-uqBaC1Ki,html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.selected-uqBaC1Ki{background:var(--themed-color-overlay-accent-1-light,#448aff33)}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki:before{background:var(--themed-color-overlay-neutral-1-light,#63636326);content:"";height:100%;left:0;opacity:0;position:fixed;top:0;width:100%}html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki:before{background:var(--themed-color-overlay-neutral-1-light,#b8b8b833)}.price-axis-currency-label-wrapper-y5H41VPj{box-sizing:border-box;padding:4px;pointer-events:none;position:absolute;-webkit-user-select:none;user-select:none;width:100%;z-index:3}.price-axis-currency-label-wrapper-y5H41VPj.hidden-y5H41VPj{visibility:hidden}.price-axis-currency-label-y5H41VPj{background:#fff;border:1px solid #ebebeb;border-radius:4px;box-sizing:border-box;cursor:default;width:100%}.price-axis-currency-label-y5H41VPj .row-y5H41VPj{align-items:center;color:#1a1a1a;column-gap:2px;display:flex;height:24px;justify-content:space-between;line-height:1em;padding:0 3px}.price-axis-currency-label-y5H41VPj .row-y5H41VPj:nth-child(1 of :not(.js-hidden)){border-top-left-radius:3px;border-top-right-radius:3px}.price-axis-currency-label-y5H41VPj .row-y5H41VPj:nth-last-child(-n+1 of :not(.js-hidden)){border-bottom-left-radius:3px;border-bottom-right-radius:3px}@media (any-hover:hover){.price-axis-currency-label-y5H41VPj .row-y5H41VPj:hover:not(.readonly){background:#f2f2f2}}.price-axis-currency-label-y5H41VPj .row-y5H41VPj.expanded-y5H41VPj{background:#ebebeb}.price-axis-currency-label-y5H41VPj .row-y5H41VPj.expanded-y5H41VPj .price-axis-currency-label-arrow-down-y5H41VPj{transform:scaleY(-1)}.price-axis-currency-label-y5H41VPj div{pointer-events:auto}.price-axis-currency-label-text-y5H41VPj{white-space:nowrap}.price-axis-currency-label-arrow-down-y5H41VPj{align-self:center;display:flex}.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj{background:#1a1a1a;border:1px solid #4a4a4a}.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj .row-y5H41VPj{color:#dbdbdb}@media (any-hover:hover){.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj .row-y5H41VPj:hover:not(.readonly){background:#303030}}.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj .row-y5H41VPj.expanded-y5H41VPj{background:#3d3d3d}.price-axis{cursor:default;height:100%;overflow:hidden;position:absolute}.price-axis--cursor-grabbing{cursor:grabbing}.price-axis--cursor-pointer{cursor:pointer}.price-axis--cursor-ns-resize{cursor:ns-resize}.price-axis__modeButtons{bottom:0;margin:0 1px;position:absolute;width:calc(100% - 2px);z-index:3}.price-axis__modeButtons_hidden{visibility:hidden}.pane{cursor:crosshair;overflow:hidden}.pane--cursor-pointer{cursor:pointer}.pane--cursor-eraser{cursor:url(eraser.c80610a04a92d2465b03.cur),default}.pane--cursor-dot{cursor:url(dot.3d617b6b01edba83a7f4.cur),default}.pane--cursor-performance{cursor:url(performance.769cf9dda2ede7d12b74.svg),default}.pane--cursor-default,.pane--cursor-demonstration{cursor:default}.pane--cursor-grabbing{cursor:grabbing}.pane--cursor-zoom-in{cursor:zoom-in}.pane--cursor-ew-resize{cursor:ew-resize}.pane--cursor-ns-resize{cursor:ns-resize}.pane--cursor-nwse-resize{cursor:nwse-resize}.pane--cursor-nesw-resize{cursor:nesw-resize}.pane--cursor-text{cursor:text}.pane--cursor-none{cursor:none}.time-axis{cursor:default}.time-axis--cursor-grabbing{cursor:grabbing}.time-axis--cursor-ew-resize{cursor:ew-resize}.chart-widget{border-style:none;box-sizing:border-box;height:256px;left:0;margin:0;overflow:hidden;padding:0;position:absolute;top:0;width:512px}.chart-markup-table{border:none;border-collapse:collapse;border-spacing:0;box-sizing:border-box;line-height:0px}.chart-gui-wrapper{align-items:flex-start;direction:ltr;display:flex;height:100%;max-height:100%;max-width:100%;overflow:hidden;position:relative;width:100%}.black-border-bigger-radius{--chart-widget-border-color:var(--themed-color-container-fill-secondary-neutral-extra-bold,#303030);--chart-widget-border-radius:4px}html.theme-dark .black-border-bigger-radius{--chart-widget-border-color:var(--themed-color-container-fill-secondary-neutral-extra-bold,#8c8c8c)}.chart-page{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#ebebeb))}html.theme-dark .chart-page{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#303030))}.chart-page .chart-container{contain:strict;position:relative}.chart-page .chart-container-border{background-color:var(--themed-color-pane-bg,#fff);border:none;height:100%;overflow:hidden;position:relative;width:100%;z-index:0}html.theme-dark .chart-page .chart-container-border{background-color:var(--themed-color-pane-bg,#1a1a1a)}.chart-page .chart-container.multiple.active:after{border:2px solid;border-color:var(--chart-widget-border-color,var(--themed-color-chart-active-outline,#2962ff));bottom:0;box-sizing:border-box;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}html.theme-dark .chart-page .chart-container.multiple.active:after{border-color:var(--chart-widget-border-color,var(--themed-color-chart-active-outline,#143eb2))}.chart-page .chart-container.inactive .back-to-present{display:none}.chart-page .chart-container.no-header-toolbar .chart-container-border{border-top-left-radius:var(--chart-widget-border-radius,4px);border-top-right-radius:var(--chart-widget-border-radius,4px)}.chart-page .chart-container.no-bottom-toolbar .chart-container-border{border-bottom-left-radius:var(--chart-widget-border-radius,4px);border-bottom-right-radius:var(--chart-widget-border-radius,4px)}.layout-with-border-radius .chart-container.multiple.active:after{border-radius:var(--chart-widget-border-radius,0)}.layout-with-border-radius .chart-container.top-left-chart .chart-container-border{border-radius:4px 0 0 0}.layout-with-border-radius .chart-container.top-left-chart.multiple.active:after{border-radius:var(--chart-widget-border-radius,4px 0 0 0)}.layout-with-border-radius .chart-container.top-right-chart .chart-container-border{border-radius:0 4px 0 0}.layout-with-border-radius .chart-container.top-right-chart.multiple.active:after{border-radius:var(--chart-widget-border-radius,0 4px 0 0)}.layout-with-border-radius .chart-container.top-full-width-chart .chart-container-border{border-radius:4px 4px 0 0}.layout-with-border-radius .chart-container.top-full-width-chart.multiple.active:after{border-radius:var(--chart-widget-border-radius,4px 4px 0 0)}.layout-with-border-radius .no-border-top-left-radius .chart-container .chart-container-border{border-top-left-radius:0}.layout-with-border-radius .no-border-top-left-radius .chart-container.multiple.active:after{border-top-left-radius:var(--chart-widget-border-radius,0)}.layout-with-border-radius .no-border-top-right-radius .chart-container .chart-container-border{border-top-right-radius:0}.layout-with-border-radius .no-border-top-right-radius .chart-container.multiple.active:after{border-top-right-radius:var(--chart-widget-border-radius,0)}.chartsSplitter-L0xapso5{background:"transparent";position:absolute}.chartsSplitter-L0xapso5.hovered-L0xapso5{background:var(--themed-color-overlay-accent-1-light,#2962ff26)}html.theme-dark .chartsSplitter-L0xapso5.hovered-L0xapso5{background:var(--themed-color-overlay-accent-1-light,#448aff33)}@font-face{font-family:EuclidCircular;font-style:normal;font-weight:400;src:url(EuclidCircular.be8f862db48c2976009f.woff2) format("woff2")} \ No newline at end of file diff --git a/frontend/charting_library/bundles/1996.25e6f30e7a095ec239f4.rtl.css b/frontend/charting_library/bundles/1996.25e6f30e7a095ec239f4.rtl.css new file mode 100644 index 0000000..ce8f448 --- /dev/null +++ b/frontend/charting_library/bundles/1996.25e6f30e7a095ec239f4.rtl.css @@ -0,0 +1 @@ +html{-webkit-text-size-adjust:100%}body{color:var(--themed-color-text-primary,#1a1a1a);font-size:14px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on}html.theme-dark body{color:var(--themed-color-text-primary,#dbdbdb)}html[lang=vi] body{font-family:Arial,sans-serif}body,html{box-sizing:border-box}body,dir,h1,h2,h3,h4,h5,h6,html,li,menu,ol,p,ul{margin:0;padding:0}a{text-decoration:none}a:active{outline:0}@media (any-hover:hover){a:hover{outline:0}}h1{font-size:2em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sup{top:-.25em}sub{bottom:-.25em}figure{margin:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button,select{text-transform:none}button,input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:default}input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:initial}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:none;margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}:root{--v-rhythm-header-1-space-phone:56px;--v-rhythm-header-1-space-tablet:80px;--v-rhythm-header-1-space-laptop:100px;--v-rhythm-header-1-space-desktop:120px;--v-rhythm-header-2-space-phone:16px;--v-rhythm-header-2-space-tablet:24px;--v-rhythm-header-2-space-laptop:32px;--v-rhythm-header-2-space-desktop:40px;--v-rhythm-header-3-space-phone:16px;--v-rhythm-header-3-space-tablet:16px;--v-rhythm-header-3-space-laptop:24px;--v-rhythm-header-3-space-desktop:24px;--v-rhythm-footer-1-space-phone:56px;--v-rhythm-footer-1-space-tablet:80px;--v-rhythm-footer-1-space-laptop:100px;--v-rhythm-footer-1-space-desktop:120px;--v-rhythm-footer-2-space-phone:16px;--v-rhythm-footer-2-space-tablet:24px;--v-rhythm-footer-2-space-laptop:32px;--v-rhythm-footer-2-space-desktop:40px;--v-rhythm-footer-3-space-phone:16px;--v-rhythm-footer-3-space-tablet:16px;--v-rhythm-footer-3-space-laptop:24px;--v-rhythm-footer-3-space-desktop:24px;--v-rhythm-spacing-1-phone:80px;--v-rhythm-spacing-1-tablet:120px;--v-rhythm-spacing-1-laptop:160px;--v-rhythm-spacing-1-desktop:200px;--v-rhythm-spacing-2-phone:64px;--v-rhythm-spacing-2-tablet:88px;--v-rhythm-spacing-2-laptop:120px;--v-rhythm-spacing-2-desktop:160px;--v-rhythm-spacing-3-phone:48px;--v-rhythm-spacing-3-tablet:64px;--v-rhythm-spacing-3-laptop:88px;--v-rhythm-spacing-3-desktop:120px;--v-rhythm-spacing-4-phone:48px;--v-rhythm-spacing-4-tablet:48px;--v-rhythm-spacing-4-laptop:64px;--v-rhythm-spacing-4-desktop:80px;--v-rhythm-spacing-5-phone:32px;--v-rhythm-spacing-5-tablet:32px;--v-rhythm-spacing-5-laptop:40px;--v-rhythm-spacing-5-desktop:48px;--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-phone);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-phone);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-phone);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-phone);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-phone);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-phone);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-phone);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-phone);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-phone);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-phone);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-phone)}@media (min-width:768px){:root{--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-tablet);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-tablet);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-tablet);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-tablet);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-tablet);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-tablet);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-tablet);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-tablet);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-tablet);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-tablet);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-tablet)}}@media (min-width:1280px){:root{--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-laptop);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-laptop);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-laptop);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-laptop);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-laptop);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-laptop);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-laptop);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-laptop);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-laptop);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-laptop);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-laptop)}}@media (min-width:1440px){:root{--v-rhythm-header-1-space:var(--v-rhythm-header-1-space-desktop);--v-rhythm-header-2-space:var(--v-rhythm-header-2-space-desktop);--v-rhythm-header-3-space:var(--v-rhythm-header-3-space-desktop);--v-rhythm-footer-1-space:var(--v-rhythm-footer-1-space-desktop);--v-rhythm-footer-2-space:var(--v-rhythm-footer-2-space-desktop);--v-rhythm-footer-3-space:var(--v-rhythm-footer-3-space-desktop);--v-rhythm-spacing-1:var(--v-rhythm-spacing-1-desktop);--v-rhythm-spacing-2:var(--v-rhythm-spacing-2-desktop);--v-rhythm-spacing-3:var(--v-rhythm-spacing-3-desktop);--v-rhythm-spacing-4:var(--v-rhythm-spacing-4-desktop);--v-rhythm-spacing-5:var(--v-rhythm-spacing-5-desktop)}}.tv-text ol,.tv-text p,.tv-text ul{color:var(--themed-color-text-primary,#1a1a1a);font-size:14px;line-height:1.6;margin-bottom:12px}html.theme-dark .tv-text ol,html.theme-dark .tv-text p,html.theme-dark .tv-text ul{color:var(--themed-color-text-primary,#dbdbdb)}.tv-text ol.tv-text__paragraph--additional-top-margin,.tv-text p.tv-text__paragraph--additional-top-margin,.tv-text ul.tv-text__paragraph--additional-top-margin{margin-top:24px}.tv-text ol.tv-text__paragraph--additional-top-margin_double,.tv-text p.tv-text__paragraph--additional-top-margin_double,.tv-text ul.tv-text__paragraph--additional-top-margin_double{margin-top:48px}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link,.tv-text p.tv-text__paragraph--additional-top-margin_double.link,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:visited,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:visited,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:visited{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link:visited,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link:visited,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link:visited{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:hover{color:var(--themed-color-link-primary-hover,#1e53e5);fill:var(--themed-color-link-primary-hover,#1e53e5)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link:hover,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link:hover,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link:hover{fill:var(--themed-color-link-primary-hover,#3179f5);color:var(--themed-color-link-primary-hover,#3179f5)}}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:active,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:active,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:active{color:var(--themed-color-link-primary-active,#1848cc);fill:var(--themed-color-link-primary-active,#1848cc)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-top-margin_double.link:active,html.theme-dark .tv-text p.tv-text__paragraph--additional-top-margin_double.link:active,html.theme-dark .tv-text ul.tv-text__paragraph--additional-top-margin_double.link:active{fill:var(--themed-color-link-primary-active,#2962ff);color:var(--themed-color-link-primary-active,#2962ff)}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:focus,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:focus,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:focus{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:focus-visible,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:focus-visible,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:focus-visible{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:focus:not(:focus-visible),.tv-text p.tv-text__paragraph--additional-top-margin_double.link:focus:not(:focus-visible),.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:focus:not(:focus-visible){outline:none}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-top-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-top-margin_double.link:hover{text-decoration:underline}}.tv-text ol.tv-text__paragraph--additional-bottom-margin,.tv-text p.tv-text__paragraph--additional-bottom-margin,.tv-text ul.tv-text__paragraph--additional-bottom-margin{margin-bottom:24px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double,.tv-text p.tv-text__paragraph--additional-bottom-margin_double,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double{margin-bottom:48px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:visited,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:visited,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:visited{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:visited,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:visited,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:visited{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:hover{color:var(--themed-color-link-primary-hover,#1e53e5);fill:var(--themed-color-link-primary-hover,#1e53e5)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:hover,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:hover,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:hover{fill:var(--themed-color-link-primary-hover,#3179f5);color:var(--themed-color-link-primary-hover,#3179f5)}}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:active,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:active,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:active{color:var(--themed-color-link-primary-active,#1848cc);fill:var(--themed-color-link-primary-active,#1848cc)}html.theme-dark .tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:active,html.theme-dark .tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:active,html.theme-dark .tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:active{fill:var(--themed-color-link-primary-active,#2962ff);color:var(--themed-color-link-primary-active,#2962ff)}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:focus,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:focus,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:focus{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:focus-visible,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:focus-visible,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:focus-visible{outline:auto;outline-offset:2px}.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:focus:not(:focus-visible),.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:focus:not(:focus-visible),.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:focus:not(:focus-visible){outline:none}@media (any-hover:hover){.tv-text ol.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text p.tv-text__paragraph--additional-bottom-margin_double.link:hover,.tv-text ul.tv-text__paragraph--additional-bottom-margin_double.link:hover{text-decoration:underline}}.tv-text h1{font-size:45px;margin-bottom:30px;margin-top:30px}@media (max-width:1019px){.tv-text h1{font-size:38px}}@media (max-width:767px){.tv-text h1{font-size:32px}}@media (max-width:479px){.tv-text h1{font-size:28px}}.tv-text h2{font-size:31px}@media (max-width:1019px){.tv-text h2{font-size:26px}}@media (max-width:479px){.tv-text h2{font-size:24px}}.tv-text h3{font-size:17px}@media (max-width:1019px){.tv-text h3{font-size:16px}}.tv-text h4{font-size:15px}.tv-text h2,.tv-text h3,.tv-text h4{margin-bottom:20px;margin-top:20px}.tv-text h1:first-child,.tv-text h2:first-child,.tv-text h3:first-child,.tv-text h4:first-child{margin-top:0}.tv-text ol,.tv-text ul{list-style-position:inside}.tv-text--position-outside ol,.tv-text--position-outside ul{list-style-position:outside;padding-right:17px}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag){color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag){fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):visited{color:var(--themed-color-link-primary-default,#2962ff);fill:var(--themed-color-link-primary-default,#2962ff)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):visited{fill:var(--themed-color-link-primary-default,#5b9cf6);color:var(--themed-color-link-primary-default,#5b9cf6)}@media (any-hover:hover){.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):hover{color:var(--themed-color-link-primary-hover,#1e53e5);fill:var(--themed-color-link-primary-hover,#1e53e5)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):hover{fill:var(--themed-color-link-primary-hover,#3179f5);color:var(--themed-color-link-primary-hover,#3179f5)}}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):active{color:var(--themed-color-link-primary-active,#1848cc);fill:var(--themed-color-link-primary-active,#1848cc)}html.theme-dark .tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):active{fill:var(--themed-color-link-primary-active,#2962ff);color:var(--themed-color-link-primary-active,#2962ff)}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):focus{outline:auto;outline-offset:2px}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):focus-visible{outline:auto;outline-offset:2px}.tv-text a:not(.tv-button):not(.tv-chart-view__description__tag):focus:not(:focus-visible){outline:none}.tv-text__font.tv-text__font--size_semilarge{font-size:15px}@media (max-width:767px){.tv-text__font.tv-text__font--size_semilarge{font-size:14px}}.tv-text__font.tv-text__font--size_large{font-size:17px}@media (max-width:767px){.tv-text__font.tv-text__font--size_large{font-size:16px}}.tv-text__font.tv-text__font--size_xlarge{font-size:19px}@media (max-width:767px){.tv-text__font.tv-text__font--size_xlarge{font-size:18px}}.tv-text__font--bold{font-weight:700}.tv-text__font--italic{font-style:italic}.tv-text--darkbg,.tv-text--darkbg ol,.tv-text--darkbg p,.tv-text--darkbg ul{color:#fff}.js-hidden{display:none!important}.js-no-pointer-events{pointer-events:none!important}.aria-live-regions-wrapper{border:0;height:1px;margin:-1px;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);overflow-x:hidden}html{color-scheme:light}html.theme-dark{color-scheme:dark}html.theme-dark iframe{color-scheme:normal}body{box-sizing:border-box;min-width:320px;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-tap-highlight-color:transparent}body.i-no-scroll{overflow:hidden!important}body.i-no-padding{padding:0!important}::selection{background:var(--themed-color-selection-bg,#bbd9fb)}html.theme-dark ::selection{background:var(--themed-color-selection-bg,#143a87)}.i-hidden{display:none!important}.i-invisible{visibility:hidden!important}.i-clearfix:after{clear:both;content:"";display:table}.i-align_left{text-align:right!important}.i-align_right{text-align:left!important}.i-align_center{text-align:center!important}.i-float_left{float:right!important}.i-float_right{float:left!important}.i-float_none{float:none!important}@media (min-width:1020px){.i-device-only{display:none!important}}@media (max-width:1019px){.i-desktop-only{display:none!important}}@media not all and (max-width:479px){.i-phones-only{display:none!important}}@media (max-width:479px){.i-except-phones-only{display:none!important}}.i-no-break{white-space:nowrap}body{overflow-y:scroll}.tv-layout-width{box-sizing:border-box;margin:0 auto;padding:0 20px;width:1020px}.tv-layout-width--no-padding{padding:0;width:980px}.tv-layout-width--simple{padding:0}.tv-dialog .tv-layout-width{max-width:100%}.chart-page .tv-layout-width{width:100%}@media (max-width:1019px){.tv-layout-width{width:auto}.tv-layout-width .tv-feed{margin-left:-20px;margin-right:-20px}.tv-layout-width .tv-feed--tablet-top-indent{margin-top:48px}}.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{width:auto;--adaptive-mf-container-padding:20px;--adaptive-mf-container-max-width:2360px;box-sizing:initial;padding-left:var(--adaptive-mf-container-padding);padding-right:var(--adaptive-mf-container-padding)}@media (min-width:1024px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:32px}}@media (min-width:1440px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:40px}}@media (min-width:1920px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:100px}}@media (min-width:2560px){.tv-layout-width.full-width-layout,body.page-wide .tv-layout-width.full-width-layout{--adaptive-mf-container-padding:calc((100vw - var(--adaptive-mf-container-max-width))/2);margin-left:auto;margin-right:auto;max-width:var(--adaptive-mf-container-max-width)}}.filter-ideas-button-with-padding{padding-inline:5px}.tv-layout-padding{padding:0 20px}body.page-wide .tv-layout-width{width:1520px}.tv-main,body.page-fullwidth .tv-layout-width{width:100%}.tv-main{display:flex;flex-direction:column;min-height:100%}.clear{clear:both}.clearfix:after{clear:both;content:"";display:table}.hide{display:none!important}.show{display:block!important}.tv-right{float:left!important}.tv-left{float:right!important}body{background-color:var(--themed-color-body-bg,#fff)}html.theme-dark body{background-color:var(--themed-color-body-bg,#000)}body.extension{min-width:0;overflow:hidden}img{border:none}textarea{resize:none}:focus{outline:none}input,textarea{border-radius:0}.tv-profile .tags,.unselectable{-webkit-user-select:none;user-select:none}.selectable,input,textarea{-webkit-user-select:text;user-select:text}.text-center{text-align:center}.loading-indicator{background:var(--themed-color-ui-loading-indicator-bg,#fff);height:100%;position:absolute;right:0;top:0;width:100%;z-index:150}html.theme-dark .loading-indicator{background:var(--themed-color-ui-loading-indicator-bg,#1a1a1a)}.falling,.growing{color:#fff}.growing{background:#42bda8}.falling{background:#f7525f}body,html{background:none;height:100%;overflow:hidden;width:100%}body{min-width:240px}#library-container{background:#fff;border:1px solid #d9dadb}#library-container #showExtendedHoursLink{display:none}.on-widget .open-popup{background:#6798bb;border-width:0!important;border:none;margin-right:4px;padding:7px}@media (any-hover:hover){.on-widget .open-popup:hover{background:#69a3cc}}.on-widget .open-popup:active{background:#71acd6}.on-widget .open-popup svg{display:block;height:14px;width:16px;fill:#fff}.on-widget .widgetbar-widget-hotlist .widgetbar-widgetheader .widgetbar-headerspace{display:none}.text .logo-highlighted{font-weight:700;text-decoration:underline}.on-cme-widget .symbol-edit-popup .filter,.on-cme-widget .symbol-search-dialog .filter{display:none}.on-cme-widget .symbol-search-dialog .results{height:450px}.load-chart .chart-search,.load-chart .chart-search input{width:100%}@media (max-width:750px){.charts-popup-list .item.save-load-chart-title{display:block}}.charts-popup-list .item .title,.charts-popup-list .item .title-expanded{width:auto}.common-tooltip-EJBD96zX{font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;font-style:normal;--ui-lib-typography-font-size:13px;font-size:var(--ui-lib-typography-font-size);font-weight:400;--ui-lib-typography-line-height:18px;color:var(--themed-color-common-tooltip-text,#f2f2f2);display:inline-flex;line-height:var(--ui-lib-typography-line-height);opacity:1;pointer-events:none;position:fixed;transition:opacity .15s linear;z-index:1000}.common-tooltip--hidden-EJBD96zX{opacity:0}.common-tooltip--horizontal-EJBD96zX{margin:4px 0}.common-tooltip--horizontal-EJBD96zX.common-tooltip--farther-EJBD96zX{margin:8px 0}.common-tooltip--vertical-EJBD96zX{margin:0 4px}.common-tooltip--vertical-EJBD96zX.common-tooltip-farther-EJBD96zX{margin:0 8px}.common-tooltip--direction_normal-EJBD96zX{flex-direction:row}.common-tooltip--direction_normal-EJBD96zX .common-tooltip__body-EJBD96zX{border-bottom-right-radius:2px;border-top-right-radius:2px}.common-tooltip--direction_normal-EJBD96zX .common-tooltip__body--no-buttons-EJBD96zX,.common-tooltip--direction_normal-EJBD96zX .common-tooltip__button-container-EJBD96zX{border-bottom-left-radius:2px;border-top-left-radius:2px}.common-tooltip--direction_normal-EJBD96zX .common-tooltip__button-EJBD96zX:not(:last-child){margin-left:1px}.common-tooltip--direction_reversed-EJBD96zX{flex-direction:row-reverse}.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__body-EJBD96zX{border-bottom-left-radius:2px;border-top-left-radius:2px}.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__body--no-buttons-EJBD96zX,.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__button-container-EJBD96zX{border-bottom-right-radius:2px;border-top-right-radius:2px}.common-tooltip--direction_reversed-EJBD96zX .common-tooltip__button-EJBD96zX:not(:first-child){margin-right:1px}.common-tooltip__ear-holder-EJBD96zX{position:relative}.common-tooltip__ear-holder-EJBD96zX:after{border:0 solid;border-color:var(--themed-color-common-tooltip-bg,#303030);box-sizing:border-box;content:"";display:block;height:0;position:absolute;width:0}html.theme-dark .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-common-tooltip-bg,#3d3d3d)}.common-tooltip__ear-holder--above-EJBD96zX:after,.common-tooltip__ear-holder--below-EJBD96zX:after{border-left:6px solid;border-left-color:var(--themed-color-tooltip-force-transparent,#0000);border-right:6px solid;border-right-color:var(--themed-color-tooltip-force-transparent,#0000);left:50%;margin-left:-6px}html.theme-dark .common-tooltip__ear-holder--above-EJBD96zX:after,html.theme-dark .common-tooltip__ear-holder--below-EJBD96zX:after{border-left-color:var(--themed-color-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-tooltip-force-transparent,#0000)}.common-tooltip__ear-holder--below-EJBD96zX:after{border-bottom-width:4px;bottom:100%}.common-tooltip__ear-holder--above-EJBD96zX:after{border-top-width:4px;top:100%}.common-tooltip__ear-holder--after-EJBD96zX:after,.common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom:6px solid;border-bottom-color:var(--themed-color-tooltip-force-transparent,#0000);border-top:6px solid;border-top-color:var(--themed-color-tooltip-force-transparent,#0000);margin-top:-6px;top:50%}html.theme-dark .common-tooltip__ear-holder--after-EJBD96zX:after,html.theme-dark .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom-color:var(--themed-color-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-tooltip-force-transparent,#0000)}.common-tooltip__ear-holder--before-EJBD96zX:after{border-right-width:4px;right:100%}.common-tooltip__ear-holder--after-EJBD96zX:after{border-left-width:4px;left:100%}.common-tooltip__body-EJBD96zX{background-color:var(--themed-color-common-tooltip-bg,#303030);box-sizing:border-box;display:block;max-width:310px;padding:3px 8px;position:relative;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;text-align:right}html.theme-dark .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-common-tooltip-bg,#3d3d3d)}.common-tooltip__body--with-hotkey-EJBD96zX{display:flex;max-width:420px;padding:0}.common-tooltip__body--width_wide-EJBD96zX{max-width:640px}.common-tooltip__body--width_narrow-EJBD96zX{max-width:200px}.common-tooltip__body--no-padding-EJBD96zX{padding:0}.common-tooltip__hotkey-block-EJBD96zX{align-items:center;color:#f2f2f2;display:inline-flex;flex:1 0 auto;justify-content:center;line-height:12px;margin:3px 0;padding-left:8px}.common-tooltip__hotkey-block--divider-EJBD96zX:before{background-color:#636363;content:"";height:100%;margin-left:8px;width:1px}.common-tooltip__hotkey-text-EJBD96zX{align-items:center;display:inline-flex;margin:3px 8px}.common-tooltip__hotkey-button-EJBD96zX{background-color:#636363;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,Trebuchet MS,Roboto,Ubuntu,sans-serif;font-feature-settings:"tnum" on,"lnum" on;height:18px;padding:1px 6px;width:auto;--ui-lib-typography-line-height:16px;line-height:var(--ui-lib-typography-line-height);--ui-lib-typography-font-size:12px;border-bottom:1px solid #8c8c8c;border-radius:4px;box-sizing:border-box;font-size:var(--ui-lib-typography-font-size);font-style:normal;font-weight:600}.common-tooltip__plus-sign-EJBD96zX{height:15px;line-height:14px;text-align:center;width:13px}.common-tooltip__button-container-EJBD96zX{display:flex;overflow:hidden;position:relative}.common-tooltip__button-EJBD96zX{align-items:center;background-color:#2962ff;color:#fff;display:flex;padding:0 10px}@media (any-hover:hover){.common-tooltip__button-EJBD96zX:hover{background-color:#bbd9fb}}.common-tooltip-EJBD96zX.theme-white{color:var(--themed-color-text-primary,#1a1a1a)}html.theme-dark .common-tooltip-EJBD96zX.theme-white{color:var(--themed-color-text-primary,#dbdbdb)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#fff);border-radius:0}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX{border:1px solid var(--themed-color-border,#ebebeb)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX{border:1px solid var(--themed-color-border,#3d3d3d)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#fff)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:after{border-left:6px solid;border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right:6px solid;border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:after{border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom:6px solid;border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top:6px solid;border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:before{border:0 solid;border-color:var(--themed-color-border,#ebebeb);content:"";display:block;height:0;position:absolute;width:0;z-index:1000}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder-EJBD96zX:before{border-color:var(--themed-color-border,#3d3d3d)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:before,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:before{border-left:7px solid;border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right:7px solid;border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000);margin-right:-7px;right:50%}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:before,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:before{border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--below-EJBD96zX:before{border-bottom-width:6px;top:-6px}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--above-EJBD96zX:before{border-top-width:6px;bottom:-6px}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:before,.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:before{border-bottom:7px solid;border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top:7px solid;border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000);margin-top:-7px;top:50%}html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:before,html.theme-dark .common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:before{border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--before-EJBD96zX:before{border-left-width:6px;right:-6px}.common-tooltip-EJBD96zX.theme-white .common-tooltip__ear-holder--after-EJBD96zX:before{border-right-width:6px;left:-6px}.common-tooltip-EJBD96zX.theme-round-shadow{box-shadow:0 1px 3px 0 #2a2c394a;color:var(--themed-color-text-primary,#1a1a1a)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow{color:var(--themed-color-text-primary,#dbdbdb)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#fff)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#fff)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder-EJBD96zX:after{border-color:var(--themed-color-bg-primary,#262626)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--above-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--below-EJBD96zX:after{border-left:6px solid;border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right:6px solid;border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--above-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--below-EJBD96zX:after{border-left-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-right-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--after-EJBD96zX:after,.common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom:6px solid;border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top:6px solid;border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--after-EJBD96zX:after,html.theme-dark .common-tooltip-EJBD96zX.theme-round-shadow .common-tooltip__ear-holder--before-EJBD96zX:after{border-bottom-color:var(--themed-color-common-tooltip-force-transparent,#0000);border-top-color:var(--themed-color-common-tooltip-force-transparent,#0000)}.common-tooltip-EJBD96zX.theme-chart .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#fff);border:1px solid var(--themed-color-divider,#ebebeb);border-radius:16px;box-shadow:0 2px 4px var(--themed-color-shadow-primary-neutral-extra-heavy,#0003);max-width:342px}html.theme-dark .common-tooltip-EJBD96zX.theme-chart .common-tooltip__body-EJBD96zX{background-color:var(--themed-color-bg-primary,#262626);border:1px solid var(--themed-color-divider,#4a4a4a);box-shadow:0 2px 4px var(--themed-color-shadow-primary-neutral-extra-heavy,#0006)}.common-tooltip-EJBD96zX.theme-chart .common-tooltip__ear-holder-EJBD96zX:after{content:none}.container-B8mkOfAH{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#ebebeb));box-sizing:border-box;height:100%;width:100%}html.theme-dark .container-B8mkOfAH{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#303030))}.container-B8mkOfAH .inner-B8mkOfAH{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#fff));height:100%;width:100%}html.theme-dark .container-B8mkOfAH .inner-B8mkOfAH{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#1a1a1a))}.container-B8mkOfAH.border-left-B8mkOfAH{padding-right:4px}.container-B8mkOfAH.border-right-B8mkOfAH{padding-left:4px}.container-B8mkOfAH.border-top-B8mkOfAH{padding-top:4px}.container-B8mkOfAH.border-bottom-B8mkOfAH{padding-bottom:4px}.container-B8mkOfAH.top-right-radius-B8mkOfAH .inner-B8mkOfAH{border-top-left-radius:0}.container-B8mkOfAH.top-left-radius-B8mkOfAH .inner-B8mkOfAH{border-top-right-radius:0}.container-B8mkOfAH.bottom-right-radius-B8mkOfAH .inner-B8mkOfAH{border-bottom-left-radius:0}.container-B8mkOfAH.bottom-left-radius-B8mkOfAH .inner-B8mkOfAH{border-bottom-right-radius:0}.chart-controls-bar{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#fff));border-top:1px solid;border-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#ebebeb));border-radius:0 0 4px 4px;box-sizing:border-box;contain:strict;height:39px;overflow:hidden;position:absolute}html.theme-dark .chart-controls-bar{background-color:var(--tv-color-pane-background,var(--themed-color-pane-bg,#1a1a1a));border-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#303030))}.no-border-bottom-left-radius .chart-controls-bar{border-bottom-left-radius:0}.no-border-bottom-right-radius .chart-controls-bar{border-bottom-right-radius:0}.tv-spinner{animation:tv-spinner__container-rotate-aLqboHuu .9s linear infinite;border-bottom-color:var(--tv-spinner-color,#2962ff);border-bottom:0 solid var(--themed-color-spinner-bg,#f2f2f2);border-left-color:var(--tv-spinner-color,#2962ff);border-left:0 solid var(--themed-color-spinner-bg,#f2f2f2);border-radius:50%;border-right:0 solid var(--tv-spinner-color,#2962ff);border-top:0 solid var(--tv-spinner-color,#2962ff);display:none;margin:0 auto;position:absolute}html.theme-dark .tv-spinner{border-bottom-color:var(--themed-color-spinner-bg,#303030);border-left-color:var(--themed-color-spinner-bg,#303030)}.tv-spinner--shown{display:block}.tv-spinner--size_xxsmall{border-width:2px;height:10px;right:calc(50% - 7px);top:calc(50% - 7px);width:10px}.tv-spinner--size_xsmall{border-width:2px;height:14px;right:calc(50% - 9px);top:calc(50% - 9px);width:14px}.tv-spinner--size_small{border-width:2px;height:20px;right:calc(50% - 12px);top:calc(50% - 12px);width:20px}.tv-spinner--size_medium{border-width:3px;height:28px;right:calc(50% - 17px);top:calc(50% - 17px);width:28px}.tv-spinner--size_large{border-width:4px;height:56px;right:calc(50% - 32px);top:calc(50% - 32px);width:56px}@keyframes tv-spinner__container-rotate-aLqboHuu{to{transform:rotate(-1turn)}}.screen-otjoFNF2{bottom:0;display:none;left:0;opacity:.5;position:absolute;right:0;top:0;z-index:8}.screen-otjoFNF2.fade-otjoFNF2{animation:screenfade-otjoFNF2 .3s ease backwards;display:block}@keyframes screenfade-otjoFNF2{0%{opacity:0}}.paneSeparator-uqBaC1Ki{margin:0;padding:0;position:relative}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki{height:7px;position:absolute;right:0;top:-3px;width:100%;z-index:50}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.mobile-uqBaC1Ki{height:11px;top:-5px}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki,.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.hovered-uqBaC1Ki,.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.selected-uqBaC1Ki{background:var(--themed-color-overlay-accent-1-light,#2962ff26);cursor:row-resize}html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki,html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.hovered-uqBaC1Ki,html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.selected-uqBaC1Ki{background:var(--themed-color-overlay-accent-1-light,#448aff33)}.paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki:before{background:var(--themed-color-overlay-neutral-1-light,#63636326);content:"";height:100%;opacity:0;position:fixed;right:0;top:0;width:100%}html.theme-dark .paneSeparator-uqBaC1Ki .handle-uqBaC1Ki.active-uqBaC1Ki:before{background:var(--themed-color-overlay-neutral-1-light,#b8b8b833)}.price-axis-currency-label-wrapper-y5H41VPj{box-sizing:border-box;padding:4px;pointer-events:none;position:absolute;-webkit-user-select:none;user-select:none;width:100%;z-index:3}.price-axis-currency-label-wrapper-y5H41VPj.hidden-y5H41VPj{visibility:hidden}.price-axis-currency-label-y5H41VPj{background:#fff;border:1px solid #ebebeb;border-radius:4px;box-sizing:border-box;cursor:default;width:100%}.price-axis-currency-label-y5H41VPj .row-y5H41VPj{align-items:center;color:#1a1a1a;column-gap:2px;display:flex;height:24px;justify-content:space-between;line-height:1em;padding:0 3px}.price-axis-currency-label-y5H41VPj .row-y5H41VPj:nth-child(1 of :not(.js-hidden)){border-top-left-radius:3px;border-top-right-radius:3px}.price-axis-currency-label-y5H41VPj .row-y5H41VPj:nth-last-child(-n+1 of :not(.js-hidden)){border-bottom-left-radius:3px;border-bottom-right-radius:3px}@media (any-hover:hover){.price-axis-currency-label-y5H41VPj .row-y5H41VPj:hover:not(.readonly){background:#f2f2f2}}.price-axis-currency-label-y5H41VPj .row-y5H41VPj.expanded-y5H41VPj{background:#ebebeb}.price-axis-currency-label-y5H41VPj .row-y5H41VPj.expanded-y5H41VPj .price-axis-currency-label-arrow-down-y5H41VPj{transform:scaleY(-1)}.price-axis-currency-label-y5H41VPj div{pointer-events:auto}.price-axis-currency-label-text-y5H41VPj{white-space:nowrap}.price-axis-currency-label-arrow-down-y5H41VPj{align-self:center;display:flex}.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj{background:#1a1a1a;border:1px solid #4a4a4a}.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj .row-y5H41VPj{color:#dbdbdb}@media (any-hover:hover){.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj .row-y5H41VPj:hover:not(.readonly){background:#303030}}.chart-widget__bottom--themed-dark .price-axis-currency-label-wrapper-y5H41VPj .price-axis-currency-label-y5H41VPj .row-y5H41VPj.expanded-y5H41VPj{background:#3d3d3d}.price-axis{cursor:default;height:100%;overflow:hidden;position:absolute}.price-axis--cursor-grabbing{cursor:grabbing}.price-axis--cursor-pointer{cursor:pointer}.price-axis--cursor-ns-resize{cursor:ns-resize}.price-axis__modeButtons{bottom:0;margin:0 1px;position:absolute;width:calc(100% - 2px);z-index:3}.price-axis__modeButtons_hidden{visibility:hidden}.pane{cursor:crosshair;overflow:hidden}.pane--cursor-pointer{cursor:pointer}.pane--cursor-eraser{cursor:url(eraser.c80610a04a92d2465b03.cur),default}.pane--cursor-dot{cursor:url(dot.3d617b6b01edba83a7f4.cur),default}.pane--cursor-performance{cursor:url(performance.769cf9dda2ede7d12b74.svg),default}.pane--cursor-default,.pane--cursor-demonstration{cursor:default}.pane--cursor-grabbing{cursor:grabbing}.pane--cursor-zoom-in{cursor:zoom-in}.pane--cursor-ew-resize{cursor:ew-resize}.pane--cursor-ns-resize{cursor:ns-resize}.pane--cursor-nwse-resize{cursor:nesw-resize}.pane--cursor-nesw-resize{cursor:nwse-resize}.pane--cursor-text{cursor:text}.pane--cursor-none{cursor:none}.time-axis{cursor:default}.time-axis--cursor-grabbing{cursor:grabbing}.time-axis--cursor-ew-resize{cursor:ew-resize}.chart-widget{border-style:none;box-sizing:border-box;height:256px;margin:0;overflow:hidden;padding:0;position:absolute;right:0;top:0;width:512px}.chart-markup-table{border:none;border-collapse:collapse;border-spacing:0;box-sizing:border-box;direction:ltr;line-height:0px}.chart-gui-wrapper{align-items:flex-start;direction:ltr;display:flex;height:100%;max-height:100%;max-width:100%;overflow:hidden;position:relative;width:100%}.black-border-bigger-radius{--chart-widget-border-color:var(--themed-color-container-fill-secondary-neutral-extra-bold,#303030);--chart-widget-border-radius:4px}html.theme-dark .black-border-bigger-radius{--chart-widget-border-color:var(--themed-color-container-fill-secondary-neutral-extra-bold,#8c8c8c)}.chart-page{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#ebebeb))}html.theme-dark .chart-page{background-color:var(--tv-color-platform-background,var(--themed-color-chart-page-bg,#303030))}.chart-page .chart-container{contain:strict;position:relative}.chart-page .chart-container-border{background-color:var(--themed-color-pane-bg,#fff);border:none;height:100%;overflow:hidden;position:relative;width:100%;z-index:0}html.theme-dark .chart-page .chart-container-border{background-color:var(--themed-color-pane-bg,#1a1a1a)}.chart-page .chart-container.multiple.active:after{border:2px solid;border-color:var(--chart-widget-border-color,var(--themed-color-chart-active-outline,#2962ff));bottom:0;box-sizing:border-box;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0}html.theme-dark .chart-page .chart-container.multiple.active:after{border-color:var(--chart-widget-border-color,var(--themed-color-chart-active-outline,#143eb2))}.chart-page .chart-container.inactive .back-to-present{display:none}.chart-page .chart-container.no-header-toolbar .chart-container-border{border-top-left-radius:var(--chart-widget-border-radius,4px);border-top-right-radius:var(--chart-widget-border-radius,4px)}.chart-page .chart-container.no-bottom-toolbar .chart-container-border{border-bottom-left-radius:var(--chart-widget-border-radius,4px);border-bottom-right-radius:var(--chart-widget-border-radius,4px)}.layout-with-border-radius .chart-container.multiple.active:after{border-radius:var(--chart-widget-border-radius,0)}.layout-with-border-radius .chart-container.top-left-chart .chart-container-border{border-radius:4px 0 0 0}.layout-with-border-radius .chart-container.top-left-chart.multiple.active:after{border-radius:var(--chart-widget-border-radius,4px 0 0 0)}.layout-with-border-radius .chart-container.top-right-chart .chart-container-border{border-radius:0 4px 0 0}.layout-with-border-radius .chart-container.top-right-chart.multiple.active:after{border-radius:var(--chart-widget-border-radius,0 4px 0 0)}.layout-with-border-radius .chart-container.top-full-width-chart .chart-container-border{border-radius:4px 4px 0 0}.layout-with-border-radius .chart-container.top-full-width-chart.multiple.active:after{border-radius:var(--chart-widget-border-radius,4px 4px 0 0)}.layout-with-border-radius .no-border-top-left-radius .chart-container .chart-container-border{border-top-left-radius:0}.layout-with-border-radius .no-border-top-left-radius .chart-container.multiple.active:after{border-top-left-radius:var(--chart-widget-border-radius,0)}.layout-with-border-radius .no-border-top-right-radius .chart-container .chart-container-border{border-top-right-radius:0}.layout-with-border-radius .no-border-top-right-radius .chart-container.multiple.active:after{border-top-right-radius:var(--chart-widget-border-radius,0)}.chartsSplitter-L0xapso5{background:"transparent";position:absolute}.chartsSplitter-L0xapso5.hovered-L0xapso5{background:var(--themed-color-overlay-accent-1-light,#2962ff26)}html.theme-dark .chartsSplitter-L0xapso5.hovered-L0xapso5{background:var(--themed-color-overlay-accent-1-light,#448aff33)}@font-face{font-family:EuclidCircular;font-style:normal;font-weight:400;src:url(EuclidCircular.be8f862db48c2976009f.woff2) format("woff2")} \ No newline at end of file diff --git a/frontend/charting_library/bundles/207.dd2de59fb0b299b4eed5.js b/frontend/charting_library/bundles/207.dd2de59fb0b299b4eed5.js new file mode 100644 index 0000000..0a1e704 --- /dev/null +++ b/frontend/charting_library/bundles/207.dd2de59fb0b299b4eed5.js @@ -0,0 +1,29 @@ +(self.webpackChunktradingview=self.webpackChunktradingview||[]).push([[207],{50151:(e,t)=>{"use strict";function n(e,t){if(void 0===e)throw new Error("".concat(null!=t?t:"Value"," is undefined"));return e}function r(e,t){if(null===e)throw new Error("".concat(null!=t?t:"Value"," is null"));return e}Object.defineProperty(t,"__esModule",{value:!0}),t.ensureNever=t.ensure=t.ensureNotNull=t.ensureDefined=t.assert=void 0,t.assert=function(e,t){if(!e)throw new Error("Assertion failed".concat(t?": ".concat(t):""))},t.ensureDefined=n,t.ensureNotNull=r,t.ensure=function(e,t){return r(n(e,t),t)},t.ensureNever=function(e){}},50335:(e,t)=>{"use strict";function n(e){return Math.round(1e10*e)/1e10}Object.defineProperty(t,"__esModule",{value:!0}),t.alignTo=t.fixComputationError=t.isNaN=t.isInteger=t.isNumber=void 0,t.isNumber=function(e){return"number"==typeof e&&isFinite(e)},t.isInteger=function(e){return"number"==typeof e&&e%1==0},t.isNaN=function(e){return!(e<=0||e>0)},t.fixComputationError=n,t.alignTo=function(e,t){var r=e/t,o=Math.floor(r),i=r-o;return i>2e-10?n(i>.5?(o+1)*t:o*t):e}},30551:(e,t)=>{"use strict";t.hasProperty=t.isObject=void 0,t.isObject=function(e){var t=typeof e;return null!==e&&("object"===t||"function"===t)},t.hasProperty=function(e,t){return t in e}},91679:(e,t)=>{"use strict";t.WatchedValue=void 0;var n=function(){function e(e,t){void 0===t&&(t={}),this._listeners=[],void 0!==e&&(this._value=e),this._onDestroy=t.onDestroy}return e.prototype.destroy=function(){this.unsubscribe(),delete this._value,delete this._readonlyInstance,this._onDestroy&&this._onDestroy()},e.prototype.value=function(){return this._value},e.prototype.setValue=function(e,t){var n=this._value===e||Number.isNaN(this._value)&&Number.isNaN(e);!t&&n||(this._value=e,this._notifyListeners())},e.prototype.subscribe=function(e,t){var n,r,o=this;if(!(null===(n=null==t?void 0:t.signal)||void 0===n?void 0:n.aborted)){if((null==t?void 0:t.callWithLast)&&void 0!==this._value){try{e(this._value)}catch(e){t.onError&&t.onError(e)}if(t.once)return}(null==t?void 0:t.signal)&&t.signal.addEventListener("abort",(function(){o.unsubscribe(e)}),{once:!0}),this._listeners.push({callback:e,signal:null==t?void 0:t.signal,once:null!==(r=null==t?void 0:t.once)&&void 0!==r&&r,onError:null==t?void 0:t.onError})}},e.prototype.unsubscribe=function(e){for(var t=this._listeners.length;t--;){e!==this._listeners[t].callback&&void 0!==e||this._listeners.splice(t,1)}},e.prototype.readonly=function(){return this._readonlyInstance||(this._readonlyInstance=new o(this)),this._readonlyInstance},e.prototype.when=function(e,t){var n,r=this;if(!e)return new Promise((function(e,t){if(void 0===r._value){var n=function(t){void 0!==t&&(e(t),r.unsubscribe(n))};r.subscribe(n,{onError:t})}else e(r._value)}));if(!(null===(n=null==t?void 0:t.signal)||void 0===n?void 0:n.aborted))if(void 0===this._value){var o=function(t){void 0!==t&&(e(t),r.unsubscribe(o))};this.subscribe(o,t)}else try{e(this._value)}catch(e){(null==t?void 0:t.onError)&&t.onError(e)}},e.prototype._notifyListeners=function(){ +for(var e,t,n=0,r=this._listeners;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.pointInCircle=t.pointInPolygon=t.pointInBox=t.pointInTriangle=t.pointInHalfplane=void 0;var r=n(5531);t.pointInHalfplane=function(e,t){var n=t.edge;return n.A*e.x+n.B*e.y+n.C>0===t.isPositive},t.pointInTriangle=function(e,t,n,o){var i=t.add(n).scaled(.5).add(o).scaled(.5),a=r.intersectLineSegments(t,n,i,e);return null===a&&(null===(a=r.intersectLineSegments(n,o,i,e))&&null===(a=r.intersectLineSegments(o,t,i,e)))},t.pointInBox=function(e,t){return e.x>=t.min.x&&e.x<=t.max.x&&e.y>=t.min.y&&e.y<=t.max.y},t.pointInPolygon=function(e,t){for(var n=t.length-1,r=!1,o=e.x,i=e.y,a=0;a=i||u.y=i)&&s.x+(i-s.y)/(u.y-s.y)*(u.x-s.x){"use strict";function n(e,t,n){var r=t.subtract(e),o=n.subtract(e).dotProduct(r)/r.dotProduct(r);return{coeff:o,distance:e.addScaled(r,o).subtract(n).length()}}Object.defineProperty(t,"__esModule",{value:!0}),t.distanceToSegment=t.distanceToLine=void 0,t.distanceToLine=n,t.distanceToSegment=function(e,t,r){var o=n(e,t,r);if(0<=o.coeff&&o.coeff<=1)return o;var i=e.subtract(r).length(),a=t.subtract(r).length();return i{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.intersectPolygons=t.intersectPolygonAndHalfplane=t.intersectRayAndBox=t.intersectLineAndBox=t.intersectLineSegments=t.intersectLines=t.intersectLineSegmentAndBox=void 0;var r=n(50151),o=n(86441),i=n(4652),a=n(34026);function s(e,t){var n=e.A,r=t.A,i=e.B,a=t.B,s=e.C,u=t.C,c=n*a-r*i;if(Math.abs(c)<1e-6)return null;var l=(i*u-a*s)/c,f=(r*s-n*u)/c;return new o.Point(l,f)}function u(e,t,n,r){var o=function(e,t,n,r){var o=t.subtract(e),i=r.subtract(n),a=o.x*i.y-o.y*i.x;if(Math.abs(a)<1e-6)return null;var s=e.subtract(n);return(s.y*i.x-s.x*i.y)/a}(e,t,n,r);if(null===o)return null;var a=t.subtract(e).scaled(o).add(e),s=i.distanceToSegment(n,r,a);return Math.abs(s.distance)<1e-6?o:null}function c(e,t){ +for(var n=0,r=e;n0&&(o.equalPoints(e[e.length-1],t)||o.equalPoints(e[0],t)))&&(e.push(t),!0)}function f(e,t){for(var n=[],r=0;r=3?n:null}t.intersectLineSegmentAndBox=function(e,t){var n=e[0].x,r=e[0].y,i=e[1].x,a=e[1].y,s=t.min.x,u=t.min.y,c=t.max.x,l=t.max.y;function f(e,t,n,r,o,i){var a=0;return eo&&(a|=2),ti&&(a|=8),a}for(var d=f(n,r,s,u,c,l),_=f(i,a,s,u,c,l),h=!1,p=0;;){if(p>1e3)throw new Error("Cohen - Sutherland algorithm: infinity loop");if(p++,!(d|_)){h=!0;break}if(d&_)break;var b=d||_,v=void 0,g=void 0;8&b?(v=n+(i-n)*(l-r)/(a-r),g=l):4&b?(v=n+(i-n)*(u-r)/(a-r),g=u):2&b?(g=r+(a-r)*(c-n)/(i-n),v=c):(g=r+(a-r)*(s-n)/(i-n),v=s),b===d?d=f(n=v,r=g,s,u,c,l):_=f(i=v,a=g,s,u,c,l)}return h?o.equalPoints(o.point(n,r),o.point(i,a))?o.point(n,r):o.lineSegment(o.point(n,r),o.point(i,a)):null},t.intersectLines=s,t.intersectLineSegments=u,t.intersectLineAndBox=function(e,t){var n=t.min.x,i=t.min.y,a=t.max.x,s=t.max.y;if(0===e.A){var u=-e.C/e.B;return i<=u&&u<=s?o.lineSegment(o.point(n,u),o.point(a,u)):null}if(0===e.B){var l=-e.C/e.A;return n<=l&&l<=a?o.lineSegment(o.point(l,i),o.point(l,s)):null}var f=[],d=function(t){var n=function(e,t){return-(e.C+e.A*t)/e.B}(e,t);i<=n&&n<=s&&c(f,new o.Point(t,n))},_=function(t){var r=function(e,t){return-(e.C+e.B*t)/e.A}(e,t);n<=r&&r<=a&&c(f,new o.Point(r,t))};switch(d(n),_(i),d(a),_(s),f.length){case 0:return null;case 1:return f[0];case 2:return o.equalPoints(f[0],f[1])?f[0]:o.lineSegment(f[0],f[1])}return r.assert(!1,"We should have at most two intersection points"),null},t.intersectRayAndBox=function(e,t,n){var r=u(e,t,n.min,new o.Point(n.max.x,n.min.y)),i=u(e,t,new o.Point(n.max.x,n.min.y),n.max),s=u(e,t,n.max,new o.Point(n.min.x,n.max.y)),c=u(e,t,new o.Point(n.min.x,n.max.y),n.min),l=[];if(null!==r&&r>=0&&l.push(r),null!==i&&i>=0&&l.push(i),null!==s&&s>=0&&l.push(s),null!==c&&c>=0&&l.push(c),0===l.length)return null;l.sort((function(e,t){return e-t}));var f=a.pointInBox(e,n)?l[0]:l[l.length-1];return e.addScaled(t.subtract(e),f)},t.intersectPolygonAndHalfplane=f,t.intersectPolygons=function(e,t){for(var n=e,r=0;r{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.equalBoxes=t.box=t.halfplaneThroughPoint=t.halfplane=t.lineSegment=t.lineThroughPoints=t.line=t.equalPoints=t.point=t.Point=void 0;var n=function(){function e(e,t){this.x=e,this.y=t}return e.prototype.add=function(t){return new e(this.x+t.x,this.y+t.y)},e.prototype.addScaled=function(t,n){return new e(this.x+n*t.x,this.y+n*t.y)},e.prototype.subtract=function(t){ +return new e(this.x-t.x,this.y-t.y)},e.prototype.dotProduct=function(e){return this.x*e.x+this.y*e.y},e.prototype.crossProduct=function(e){return this.x*e.y-this.y*e.x},e.prototype.signedAngle=function(e){return Math.atan2(this.crossProduct(e),this.dotProduct(e))},e.prototype.angle=function(e){return Math.acos(this.dotProduct(e)/(this.length()*e.length()))},e.prototype.length=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},e.prototype.scaled=function(t){return new e(this.x*t,this.y*t)},e.prototype.normalized=function(){return this.scaled(1/this.length())},e.prototype.transposed=function(){return new e(-this.y,this.x)},e.prototype.clone=function(){return new e(this.x,this.y)},e}();function r(e,t){return new n(e,t)}function o(e,t){return e.x===t.x&&e.y===t.y}function i(e,t,n){if(0===e&&0===t)throw new Error("A and B can not be both equal to zero.");return{A:e,B:t,C:n}}function a(e,t){return{edge:e,isPositive:t}}t.Point=n,t.point=r,t.equalPoints=o,t.line=i,t.lineThroughPoints=function(e,t){if(o(e,t))throw new Error("Points should be distinct");return i(e.y-t.y,t.x-e.x,e.x*t.y-t.x*e.y)},t.lineSegment=function(e,t){if(o(e,t))throw new Error("Points of a segment should be distinct");return[e,t]},t.halfplane=a,t.halfplaneThroughPoint=function(e,t){return a(e,e.A*t.x+e.B*t.y+e.C>0)},t.box=function(e,t){return{min:r(Math.min(e.x,t.x),Math.min(e.y,t.y)),max:r(Math.max(e.x,t.x),Math.max(e.y,t.y))}},t.equalBoxes=function(e,t){return o(e.min,t.min)&&o(e.max,t.max)}},24377:(e,t,n)=>{"use strict";var r=n(50335);function o(e,t,n){return r.isNaN(t)||tn?n:Math.round(t)}function i(e,t,n){return r.isNaN(t)||tn?n:Math.round(1e4*t)/1e4}function a(e){return o(0,e,255)}function s(e){return o(0,e,255)}function u(e){return o(0,e,255)}function c(e){return i(0,e,1)}function l(e){return i(0,e,1)}function f(e){return i(0,e,1)}function d(e){return i(0,e,1)}function _(e){return i(0,e,1)}function h(e){return i(0,e,1)}function p(e){var t=e[0]/255,n=e[1]/255,r=e[2]/255,o=Math.min(t,n,r),i=Math.max(t,n,r),a=0,s=0,u=(o+i)/2;if(o===i)a=0,s=0;else{var c=i-o;switch(s=u>.5?c/(2-i-o):c/(i+o),i){case t:a=((n-r)/c+(n1&&(n-=1),n<1/6?e+6*(t-e)*n:n<.5?t:n<2/3?e+(t-e)*(2/3-n)*6:e}function v(e){var t,n,r,o=e[0],i=e[1],c=e[2];if(0===i)t=n=r=c;else{var l=c<.5?c*(1+i):c+i-c*i,f=2*c-l;t=b(f,l,o+1/3),n=b(f,l,o),r=b(f,l,o-1/3)}return[a(255*t),s(255*n),u(255*r)]}t.normalizeAlphaComponent=c,t.areEqualRgb=function(e,t){return e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]},t.rgba=function(e,t,n,r){if(Array.isArray(e)){var o=e;return r=t,[o[0],o[1],o[2],c(r)]}var i=t;return n=n||0,r=r||0,[a(e),s(i),u(n),c(r)]},t.areEqualRgba=function(e,t){return e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&e[3]===t[3]},t.rgbToHsl=p,t.hslToRgb=v;var g=[.199,.687,.114];function m(e){return g[0]*e[0]+g[1]*e[1]+g[2]*e[2]}function y(e,t,n){void 0===n&&(n=.05);var r=p(e),o=r[0]+t*n;return r[0]=l(o-Math.floor(o)),v(r)}function w(e,t,n){void 0===n&&(n=.05) +;var r=e[0],o=e[1],i=e[2],a=e[3],s=y([r,o,i],t,n);return[s[0],s[1],s[2],a]}t.distanceRgb=function(e,t){var n=e[0],r=e[1],o=e[2],i=t[0]-n,a=t[1]-r,s=t[2]-o;return Math.sqrt(i*i+a*a+s*s)},t.invertRgb=function(e){return[255-e[0],255-e[1],255-e[2]]},t.blendRgba=function(e,t){var n=e[0],r=e[1],o=e[2],i=e[3],l=t[0],f=t[1],d=t[2],_=t[3],h=c(1-(1-_)*(1-i));return[a(l*_/h+n*i*(1-_)/h),s(f*_/h+r*i*(1-_)/h),u(d*_/h+o*i*(1-_)/h),h]},t.shiftRgb=y,t.shiftRgba=w,t.shiftColor=function(e,t,n){return void 0===n&&(n=.05),L(w(B(e),t,n))};var x,j,E,S,O={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dodgerblue:"#1e90ff",feldspar:"#d19275",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslateblue:"#8470ff",lightslategray:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0", +skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",violetred:"#d02090",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};function z(e,t){return t in e}function A(e){var t=x.re.exec(e);return null!==t?x.parse(t):null}function P(e){var t=j.re.exec(e);return null!==t?j.parse(t):null}function k(e){var t=E.re.exec(e);return null!==t?E.parse(t):null}function R(e){var t=S.re.exec(e);return null!==t?S.parse(t):null}function L(e){return"rgba("+e[0]+", "+e[1]+", "+e[2]+", "+e[3]+")"}function C(e){if(e=e.toLowerCase(),z(O,e)){var t=P(O[e]);if(null!==t)return t;throw new Error("Invalid named color definition")}var n=A(e);if(null!==n)return n;var r=P(e);if(null!==r)return r;var o=k(e);if(null!==o)return o;var i=R(e);return null!==i?[i[0],i[1],i[2]]:null}function N(e){if(e=e.toLowerCase(),z(O,e)){var t=P(O[e]);if(null!==t)return[t[0],t[1],t[2],1];throw new Error("Invalid named color definition")}var n=A(e);if(null!==n)return[n[0],n[1],n[2],1];var r=P(e);if(null!==r)return[r[0],r[1],r[2],1];var o=k(e);if(null!==o)return[o[0],o[1],o[2],1];var i=R(e);return null!==i?i:null}function B(e){var t=N(e);if(null!==t)return t;throw new Error("Passed color string does not match any of the known color representations")}!function(e){e.re=/^rgb\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*\)$/,e.parse=function(e){return[a(parseInt(e[1],10)),s(parseInt(e[2],10)),u(parseInt(e[3],10))]}}(x||(x={})),function(e){e.re=/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,e.parse=function(e){return[a(parseInt(e[1],16)),s(parseInt(e[2],16)),u(parseInt(e[3],16))]}}(j||(j={})),t.rgbToHexString=function(e){var t=e[0],n=e[1],r=e[2],o=t.toString(16),i=n.toString(16),a=r.toString(16);return"#"+(1===o.length?"0":"")+o+(1===i.length?"0":"")+i+(1===a.length?"0":"")+a},function(e){e.re=/^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/,e.parse=function(e){return[a(parseInt(e[1]+e[1],16)),s(parseInt(e[2]+e[2],16)),u(parseInt(e[3]+e[3],16))]}}(E||(E={})),function(e){e.re=/^rgba\(\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?\d{1,10})\s*,\s*(-?[\d]{0,10}(?:\.\d+)?)\s*\)$/,e.parse=function(e){return[a(parseInt(e[1],10)),s(parseInt(e[2],10)),u(parseInt(e[3],10)),c(parseFloat(e[4]))]}}(S||(S={})),t.rgbaToString=L,t.rgbToBlackWhiteString=function(e,t){if(t<0||t>255)throw new Error("invalid threshold value, valid values are [0, 255]");return m(e)>=t?"white":"black"},t.parseRgb=function(e){var t=C(e);if(null!==t)return t;throw new Error("Passed color string does not match any of the known color representations")},t.tryParseRgba=N,t.parseRgba=B},60521:function(e,t,n){var r;!function(){"use strict";var o,i=1e6,a=1e6,s="[big.js] ",u=s+"Invalid ",c=u+"decimal places",l=u+"rounding mode",f=s+"Division by zero",d={},_=void 0,h=/^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;function p(e,t,n,r){var o=e.c;if(n===_&&(n=e.constructor.RM),0!==n&&1!==n&&2!==n&&3!==n)throw Error(l) +;if(t<1)r=3===n&&(r||!!o[0])||0===t&&(1===n&&o[0]>=5||2===n&&(o[0]>5||5===o[0]&&(r||o[1]!==_))),o.length=1,r?(e.e=e.e-t+1,o[0]=1):o[0]=e.e=0;else if(t=5||2===n&&(o[t]>5||5===o[t]&&(r||o[t+1]!==_||1&o[t-1]))||3===n&&(r||!!o[0]),o.length=t--,r)for(;++o[t]>9;)o[t]=0,t--||(++e.e,o.unshift(1));for(t=o.length;!o[--t];)o.pop()}return e}function b(e,t,n){var r=e.e,o=e.c.join(""),i=o.length;if(t)o=o.charAt(0)+(i>1?"."+o.slice(1):"")+(r<0?"e":"e+")+r;else if(r<0){for(;++r;)o="0"+o;o="0."+o}else if(r>0)if(++r>i)for(r-=i;r--;)o+="0";else r1&&(o=o.charAt(0)+"."+o.slice(1));return e.s<0&&n?"-"+o:o}d.abs=function(){var e=new this.constructor(this);return e.s=1,e},d.cmp=function(e){var t,n=this,r=n.c,o=(e=new n.constructor(e)).c,i=n.s,a=e.s,s=n.e,u=e.e;if(!r[0]||!o[0])return r[0]?i:o[0]?-a:0;if(i!=a)return i;if(t=i<0,s!=u)return s>u^t?1:-1;for(a=(s=r.length)<(u=o.length)?s:u,i=-1;++io[i]^t?1:-1;return s==u?0:s>u^t?1:-1},d.div=function(e){var t=this,n=t.constructor,r=t.c,o=(e=new n(e)).c,a=t.s==e.s?1:-1,s=n.DP;if(s!==~~s||s<0||s>i)throw Error(c);if(!o[0])throw Error(f);if(!r[0])return e.s=a,e.c=[e.e=0],e;var u,l,d,h,b,v=o.slice(),g=u=o.length,m=r.length,y=r.slice(0,u),w=y.length,x=e,j=x.c=[],E=0,S=s+(x.e=t.e-e.e)+1;for(x.s=a,a=S<0?0:S,v.unshift(0);w++w?1:-1;else for(b=-1,h=0;++by[b]?1:-1;break}if(!(h<0))break;for(l=w==u?o:v;w;){if(y[--w]S&&p(x,S,n.RM,y[0]!==_),x},d.eq=function(e){return 0===this.cmp(e)},d.gt=function(e){return this.cmp(e)>0},d.gte=function(e){return this.cmp(e)>-1},d.lt=function(e){return this.cmp(e)<0},d.lte=function(e){return this.cmp(e)<1},d.minus=d.sub=function(e){var t,n,r,o,i=this,a=i.constructor,s=i.s,u=(e=new a(e)).s;if(s!=u)return e.s=-u,i.plus(e);var c=i.c.slice(),l=i.e,f=e.c,d=e.e;if(!c[0]||!f[0])return f[0]?e.s=-u:c[0]?e=new a(i):e.s=1,e;if(s=l-d){for((o=s<0)?(s=-s,r=c):(d=l,r=f),r.reverse(),u=s;u--;)r.push(0);r.reverse()}else for(n=((o=c.length0)for(;u--;)c[t++]=0;for(u=t;n>s;){if(c[--n]0?(u=a, +r=c):(t=-t,r=s),r.reverse();t--;)r.push(0);r.reverse()}for(s.length-c.length<0&&(r=c,c=s,s=r),t=c.length,n=0;t;s[t]%=10)n=(s[--t]=s[t]+c[t]+n)/10|0;for(n&&(s.unshift(n),++u),t=s.length;0===s[--t];)s.pop();return e.c=s,e.e=u,e},d.pow=function(e){var t=this,n=new t.constructor("1"),r=n,o=e<0;if(e!==~~e||e<-1e6||e>a)throw Error(u+"exponent");for(o&&(e=-e);1&e&&(r=r.times(t)),e>>=1;)t=t.times(t);return o?n.div(r):r},d.prec=function(e,t){if(e!==~~e||e<1||e>i)throw Error(u+"precision");return p(new this.constructor(this),e,t)},d.round=function(e,t){if(e===_)e=0;else if(e!==~~e||e<-i||e>i)throw Error(c);return p(new this.constructor(this),e+this.e+1,t)},d.sqrt=function(){var e,t,n,r=this,o=r.constructor,i=r.s,a=r.e,u=new o("0.5");if(!r.c[0])return new o(r);if(i<0)throw Error(s+"No square root");0===(i=Math.sqrt(r+""))||i===1/0?((t=r.c.join("")).length+a&1||(t+="0"),a=((a+1)/2|0)-(a<0||1&a),e=new o(((i=Math.sqrt(t))==1/0?"5e":(i=i.toExponential()).slice(0,i.indexOf("e")+1))+a)):e=new o(i+""),a=e.e+(o.DP+=4);do{n=e,e=u.times(n.plus(r.div(n)))}while(n.c.slice(0,a).join("")!==e.c.slice(0,a).join(""));return p(e,(o.DP-=4)+e.e+1,o.RM)},d.times=d.mul=function(e){var t,n=this,r=n.constructor,o=n.c,i=(e=new r(e)).c,a=o.length,s=i.length,u=n.e,c=e.e;if(e.s=n.s==e.s?1:-1,!o[0]||!i[0])return e.c=[e.e=0],e;for(e.e=u+c,au;)s=t[c]+i[u]*o[c-u-1]+s,t[c--]=s%10,s=s/10|0;t[c]=s}for(s?++e.e:t.shift(),u=t.length;!t[--u];)t.pop();return e.c=t,e},d.toExponential=function(e,t){var n=this,r=n.c[0];if(e!==_){if(e!==~~e||e<0||e>i)throw Error(c);for(n=p(new n.constructor(n),++e,t);n.c.lengthi)throw Error(c);for(e=e+(n=p(new n.constructor(n),e+n.e+1,t)).e+1;n.c.length=t.PE,!!e.c[0])},d.toNumber=function(){var e=Number(b(this,!0,!0));if(!0===this.constructor.strict&&!this.eq(e.toString()))throw Error(s+"Imprecise conversion");return e},d.toPrecision=function(e,t){var n=this,r=n.constructor,o=n.c[0];if(e!==_){if(e!==~~e||e<1||e>i)throw Error(u+"precision");for(n=p(new r(n),e,t);n.c.length=r.PE,!!o)},d.valueOf=function(){var e=this,t=e.constructor;if(!0===t.strict)throw Error(s+"valueOf disallowed");return b(e,e.e<=t.NE||e.e>=t.PE,!0)},o=function e(){function t(n){var r=this;if(!(r instanceof t))return n===_?e():new t(n);if(n instanceof t)r.s=n.s,r.e=n.e,r.c=n.c.slice();else{if("string"!=typeof n){if(!0===t.strict)throw TypeError(u+"number");n=0===n&&1/n<0?"-0":String(n)}!function(e,t){var n,r,o;if(!h.test(t))throw Error(u+"number");e.s="-"==t.charAt(0)?(t=t.slice(1),-1):1,(n=t.indexOf("."))>-1&&(t=t.replace(".",""));(r=t.search(/e/i))>0?(n<0&&(n=r),n+=+t.slice(r+1),t=t.substring(0,r)):n<0&&(n=t.length);for(o=t.length,r=0;r0&&"0"==t.charAt(--o););for(e.e=n-r-1,e.c=[],n=0;r<=o;)e.c[n++]=+t.charAt(r++)}}(r,n)}r.constructor=t}return t.prototype=d,t.DP=20,t.RM=1,t.NE=-7,t.PE=21,t.strict=false,t.roundDown=0,t.roundHalfUp=1,t.roundHalfEven=2,t.roundUp=3,t}(),o.default=o.Big=o,void 0===(r=function(){return o}.call(t,n,t,e))||(e.exports=r)}()},64531:(e,t)=>{"use strict";var n,r=!("undefined"==typeof window||!window.document||!window.document.createElement);function o(){if(n)return n;if(!r||!window.document.body)return"indeterminate";var e=window.document.createElement("div");return e.appendChild(document.createTextNode("ABCD")),e.dir="rtl",e.style.fontSize="14px",e.style.width="4px",e.style.height="1px",e.style.position="absolute",e.style.top="-1000px",e.style.overflow="scroll",document.body.appendChild(e),n="reverse",e.scrollLeft>0?n="default":(e.scrollLeft=1,0===e.scrollLeft&&(n="negative")),document.body.removeChild(e),n}t.detectScrollType=o,t.getNormalizedScrollLeft=function(e,t){var n=e.scrollLeft;if("rtl"!==t)return n;var r=o();if("indeterminate"===r)return Number.NaN;switch(r){case"negative":return e.scrollWidth-e.clientWidth+n;case"reverse":return e.scrollWidth-e.clientWidth-n}return n}},32563:(e,t,n)=>{"use strict";n.d(t,{mobiletouch:()=>o,setClasses:()=>a,touch:()=>i});var r=n(75774);const o=r.mobiletouch,i=r.touch;function a(){document.documentElement.classList.add(r.touch?"feature-touch":"feature-no-touch",r.mobiletouch?"feature-mobiletouch":"feature-no-mobiletouch")}},49483:(e,t,n)=>{"use strict";n.r(t),n.d(t,{CheckMobile:()=>d,appVersion:()=>f,checkPageType:()=>p,desktopAppVersion:()=>l,isChrome:()=>b,isDesktopApp:()=>u,isEdge:()=>g,isFF:()=>v,isLinux:()=>s,isMac:()=>i,isSafari:()=>m,isSymphonyEmbed:()=>c,isWindows:()=>a,onGoPro:()=>y,onMainPage:()=>w,onWidget:()=>_,supportTouch:()=>h});var r=n(75774);const o=window.TradingView=window.TradingView||{};function i(){return r.isMac}function a(){return r.isWindows}function s(){return r.isLinux}function u(){return/TVDesktop/i.test(navigator.userAgent)}function c(){return o.isSymphony||!1}function l(){const e=navigator.userAgent.match(/TVDesktop\/([^\s]+)/);return e&&e[1]}function f(){const e=navigator.userAgent.match(/TradingView\/([^\s]+)/);return e&&e[1]}const d={Android:()=>r.isAndroid,BlackBerry:()=>r.isBlackBerry,iOS:()=>r.isIOS,Opera:()=>r.isOperaMini,isIPad:()=>r.isIPad,any:()=>r.isAnyMobile};function _(){ +const e=["^widgetembed/?$","^cmewidgetembed/?$","^([0-9a-zA-Z-]+)/widgetembed/?$","^([0-9a-zA-Z-]+)/widgetstatic/?$","^([0-9a-zA-Z-]+)?/?mediumwidgetembed/?$","^twitter-chart/?$","^embed/([0-9a-zA-Z]{8})/?$","^widgetpopup/?$","^extension/?$","^idea-popup/?$","^hotlistswidgetembed/?$","^([0-9a-zA-Z-]+)/hotlistswidgetembed/?$","^marketoverviewwidgetembed/?$","^([0-9a-zA-Z-]+)/marketoverviewwidgetembed/?$","^eventswidgetembed/?$","^tickerswidgetembed/?$","^forexcrossrateswidgetembed/?$","^forexheatmapwidgetembed/?$","^marketquoteswidgetembed/?$","^screenerwidget/?$","^cryptomktscreenerwidget/?$","^([0-9a-zA-Z-]+)/cryptomktscreenerwidget/?$","^([0-9a-zA-Z-]+)/marketquoteswidgetembed/?$","^technical-analysis-widget-embed/$","^singlequotewidgetembed/?$","^([0-9a-zA-Z-]+)/singlequotewidgetembed/?$","^embed-widget/([0-9a-zA-Z-]+)/(([0-9a-zA-Z-]+)/)?$"],t=window.location.pathname.replace(/^\//,"");let n;for(let r=e.length-1;r>=0;r--)if(n=new RegExp(e[r]),n.test(t))return!0;return!1}function h(){return r.mobiletouch||r.touch||r.isAnyMobile}function p(e){return new URLSearchParams(window.location.search).get("page_type")===e}o.isMobile=d,o.onWidget=_;const b=r.isChrome,v=r.isFF,g=r.isEdge,m=r.isSafari;function y(){return"/pricing/"===window.location.pathname}function w(){return"/"===window.location.pathname}},11542:(e,t,n)=>{"use strict";n.r(t),n.d(t,{t:()=>r.t,withTranslationContext:()=>o});n(21251);var r=n(7029);function o(e){throw new Error("Not implemented")}},28865:(e,t,n)=>{"use strict";n.d(t,{getIsoLanguageCodeFromLanguage:()=>o});const r={ar_AE:"ar",br:"pt",de_DE:"de",ca_ES:"ca",he_IL:"he",id_ID:"id",in:"en",kr:"ko",ms_MY:"ms",sv_SE:"sv",th_TH:"th",uk:"en",vi_VN:"vi",zh_CN:"zh-Hans",zh_TW:"zh-Hant",zh:"zh-Hans",hu_HU:"hu-HU"};function o(e){return r[e]||e}},87795:e=>{"use strict";const t=55296,n=127995,r=127999,o=[776,2359,2359,2367,2367,2984,3007,3021,3633,3635,3648,3657,4352,4449,4520];function i(e){if("string"!=typeof e)throw new Error("string cannot be undefined or null");const t=[];let n=0,r=0;for(;n=t&&e<=n}e.exports=i,e.exports.substr=function(e,t,n){const r=i(e);if(void 0===t)return e;if(t>=r.length)return"";const o=r.length-t;let a=t+(void 0===n?o:n);return a>t+o&&(a=void 0),r.slice(t,a).join("")}},56570:(e,t,n)=>{"use strict";n.r(t),n.d(t,{disable:()=>f, +enable:()=>l,enabled:()=>u,getAllFeatures:()=>d,setEnabled:()=>c}) +;const r=JSON.parse('{"14851":{},"custom_items_in_context_menu":{},"countdown":{},"symbol_search_parser_mixin":{},"pay_attention_to_ticker_not_symbol":{},"graying_disabled_tools_enabled":{},"update_study_formatter_on_symbol_resolve":{},"constraint_dialogs_movement":{},"phone_verification":{},"show_trading_notifications_history":{},"show_interval_dialog_on_key_press":{},"header_interval_dialog_button":{"subsets":["show_interval_dialog_on_key_press"]},"header_fullscreen_button":{},"header_symbol_search":{},"symbol_search_hot_key":{},"header_resolutions":{"subsets":["header_interval_dialog_button"]},"header_chart_type":{},"header_settings":{},"header_indicators":{},"header_compare":{},"header_undo_redo":{},"header_quick_search":{},"header_screenshot":{},"header_saveload":{},"study_on_study":{},"scales_date_format":{},"scales_time_hours_format":{},"header_widget":{"subsets":["header_widget_dom_node","header_symbol_search","header_resolutions","header_chart_type","header_settings","header_indicators","header_compare","header_undo_redo","header_quick_search","header_fullscreen_button","compare_symbol","header_screenshot"]},"legend_widget":{},"compare_symbol":{"subsets":["header_compare"]},"property_pages":{"subsets":["show_chart_property_page","chart_property_page"]},"show_chart_property_page":{},"chart_property_page":{"subsets":["chart_property_page_scales","chart_property_page_trading","chart_property_page_right_margin_editor"]},"left_toolbar":{},"right_toolbar":{},"hide_left_toolbar_by_default":{},"control_bar":{},"widget_logo":{},"timeframes_toolbar":{},"edit_buttons_in_legend":{"subsets":["show_hide_button_in_legend","format_button_in_legend","study_buttons_in_legend","delete_button_in_legend","legend_inplace_edit"]},"show_hide_button_in_legend":{},"object_tree_legend_mode":{},"format_button_in_legend":{},"study_buttons_in_legend":{},"delete_button_in_legend":{},"legend_inplace_edit":{},"broker_button":{},"buy_sell_buttons":{"subsets":["broker_button"]},"pane_context_menu":{},"scales_context_menu":{},"legend_context_menu":{},"context_menus":{"subsets":["pane_context_menu","scales_context_menu","legend_context_menu","objects_tree_context_menu"]},"items_favoriting":{},"save_chart_properties_to_local_storage":{},"use_localstorage_for_settings":{"subsets":["items_favoriting","save_chart_properties_to_local_storage"]},"handle_scale":{"subsets":["mouse_wheel_scale","pinch_scale","axis_pressed_mouse_move_scale"]},"handle_scroll":{"subsets":["mouse_wheel_scroll","pressed_mouse_move_scroll","horz_touch_drag_scroll","vert_touch_drag_scroll"]},"plain_studymarket":{},"disable_resolution_rebuild":{},"border_around_the_chart":{},"charting_library_debug_mode":{},"saveload_requires_authentication":{},"saveload_storage_customization":{},"volume_force_overlay":{},"create_volume_indicator_by_default":{},"create_volume_indicator_by_default_once":{},"saved_charts_count_restriction":{},"lean_chart_load":{},"stop_study_on_restart":{},"star_some_intervals_by_default":{},"move_logo_to_main_pane":{},"show_animated_logo":{},"link_to_tradingview":{},"logo_without_link":{},"logo_always_maximized":{},"right_bar_stays_on_scroll":{},"chart_content_overrides_by_defaults":{},"snapshot_trading_drawings":{},"allow_supported_resolutions_set_only":{},"widgetbar_tabs":{"subsets":["right_toolbar"]},"show_object_tree":{"subsets":["right_toolbar"]},"dom_widget":{"subsets":["right_toolbar"]},"collapsible_header":{},"study_templates":{},"side_toolbar_in_fullscreen_mode":{},"header_in_fullscreen_mode":{},"remove_library_container_border":{},"whotrades_auth_only":{},"support_multicharts":{},"display_market_status":{},"display_data_mode":{},"datasource_copypaste":{},"drawing_templates":{"subsets":["linetoolpropertieswidget_template_button"]},"expand_symbolsearch_items":{},"symbol_search_three_columns_exchanges":{},"symbol_search_flags":{},"symbol_search_limited_exchanges":{},"bugreport_button":{"subsets":["right_toolbar"]},"footer_publish_idea_button":{},"text_notes":{},"show_source_code":{},"symbol_info":{},"no_bars_status":{},"clear_bars_on_series_error":{},"hide_loading_screen_on_series_error":{},"seconds_resolution":{},"dont_show_boolean_study_arguments":{},"hide_last_na_study_output":{},"price_scale_always_last_bar_value":{},"study_dialog_fundamentals_economy_addons":{},"uppercase_instrument_names":{},"trading_notifications":{},"chart_crosshair_menu":{},"japanese_chart_styles":{},"hide_series_legend_item":{},"hide_study_overlay_legend_item":{},"hide_study_compare_legend_item":{},"linetoolpropertieswidget_template_button":{},"use_overrides_for_overlay":{},"timezone_menu":{},"main_series_scale_menu":{},"show_login_dialog":{},"remove_img_from_rss":{},"bars_marks":{},"chart_scroll":{},"chart_zoom":{},"source_selection_markers":{},"low_density_bars":{},"end_of_period_timescale_marks":{},"open_account_manager":{},"show_order_panel_on_start":{},"order_panel":{"subsets":["order_panel_close_button","order_panel_undock","right_toolbar","order_info"]},"multiple_watchlists":{},"watchlist_import_export":{},"study_overlay_compare_legend_option":{},"mobile_app_action_open_details_webview":{},"custom_resolutions":{},"referral_program_for_widget_owners":{},"mobile_trading":{},"real_brokers":{},"no_min_chart_width":{},"lock_visible_time_range_on_resize":{},"pricescale_currency":{},"cropped_tick_marks":{},"trading_account_manager":{},"disable_sameinterval_aligning":{},"display_legend_on_all_charts":{},"chart_style_hilo":{},"chart_style_hilo_last_price":{},"pricescale_unit":{},"show_spread_operators":{},"hide_exponentiation_spread_operator":{},"hide_reciprocal_spread_operator":{},"compare_symbol_search_spread_operators":{},"studies_symbol_search_spread_operators":{},"hide_resolution_in_legend":{},"hide_unresolved_symbols_in_legend":{},"fix_left_edge":{},"study_symbol_ticker_description":{},"two_character_bar_marks_labels":{},"tick_resolution":{},"secondary_series_extend_time_scale":{},"hide_volume_ma":{},"small_no_display":{},"charting_library_single_symbol_request":{},"use_ticker_on_symbol_info_update":{},"show_zoom_and_move_buttons_on_touch":{},"hide_main_series_symbol_from_indicator_legend":{},"chart_hide_close_position_button":{},"chart_hide_close_order_button":{},"hide_price_scale_global_last_bar_value":{"subsets":["use_last_visible_bar_value_in_legend"]},"keep_object_tree_widget_in_right_toolbar":{},"show_average_close_price_line_and_label":{},"hide_image_invalid_symbol":{},"hide_object_tree_and_price_scale_exchange_label":{},"confirm_overwrite_if_chart_layout_with_name_exists":{},"determine_first_data_request_size_using_visible_range":{},"use_na_string_for_not_available_values":{},"show_last_price_and_change_only_in_series_legend":{},"legend_last_day_change":{},"iframe_loading_compatibility_mode":{},"show_percent_option_for_right_margin":{},"watchlist_context_menu":{},"accessible_keyboard_shortcuts":{},"advanced_emoji_in_titles":{},"app_phone":{},"app_tablet":{},"mobile_app_hide_replay_toolbar":{},"symbol_search_option_chain_selector":{},"tv_production":{"subsets":["advanced_emoji_in_titles","auto_enable_symbol_labels","symbol_search_parser_mixin","header_fullscreen_button","header_widget","dont_show_boolean_study_arguments","left_toolbar","right_toolbar","buy_sell_buttons","control_bar","symbol_search_hot_key","context_menus","edit_buttons_in_legend","object_tree_legend_mode","uppercase_instrument_names","use_localstorage_for_settings","saveload_requires_authentication","volume_force_overlay","saved_charts_count_restriction","create_volume_indicator_by_default","create_volume_indicator_by_default_once","charts_auto_save","save_old_chart_before_save_as","chart_content_overrides_by_defaults","alerts","header_saveload","header_layouttoggle","datasource_copypaste","show_saved_watchlists","watchlists_from_to_file","add_to_watchlist","property_pages","support_multicharts","display_market_status","display_data_mode","show_chart_warn_message","support_manage_drawings","widgetbar_tabs","study_templates","collapsible_header","drawing_templates","footer_publish_idea_button","text_notes","show_source_code","symbol_info","linetoolpropertieswidget_template_button","trading_notifications","symbol_search_three_columns_exchanges","symbol_search_flags","symbol_search_limited_exchanges","phone_verification","custom_resolutions","compare_symbol","study_on_study","japanese_chart_styles","show_login_dialog","dom_widget","bars_marks","chart_scroll","chart_zoom","show_trading_notifications_history","source_selection_markers","study_dialog_fundamentals_economy_addons","multiple_watchlists","marked_symbols","order_panel","pricescale_currency","show_animated_logo","pricescale_currency","show_object_tree","watchlist_import_export","scales_date_format","scales_time_hours_format","popup_hints","show_right_widgets_panel_by_default","compare_recent_symbols_enabled","chart_style_hilo_last_price","symbol_search_option_chain_selector"]},"widget":{"subsets":["auto_enable_symbol_labels","symbol_search_parser_mixin","uppercase_instrument_names","left_toolbar","right_toolbar","control_bar","symbol_search_hot_key","context_menus","edit_buttons_in_legend","object_tree_legend_mode","use_localstorage_for_settings","saveload_requires_authentication","volume_force_overlay","create_volume_indicator_by_default","create_volume_indicator_by_default_once","dont_show_boolean_study_arguments","header_widget_dom_node","header_symbol_search","header_resolutions","header_chart_type","header_compare","header_indicators","star_some_intervals_by_default","display_market_status","display_data_mode","show_chart_warn_message","symbol_info","linetoolpropertieswidget_template_button","symbol_search_three_columns_exchanges","symbol_search_flags","symbol_search_limited_exchanges","widgetbar_tabs","compare_symbol","show_login_dialog","plain_studymarket","japanese_chart_styles","bars_marks","chart_scroll","chart_zoom","source_selection_markers","property_pages","show_right_widgets_panel_by_default","chart_style_hilo_last_price"]},"bovespa_widget":{"subsets":["widget","header_settings","linetoolpropertieswidget_template_button","compare_recent_symbols_enabled"]},"charting_library_base":{"subsets":["14851","allow_supported_resolutions_set_only","auto_enable_symbol_labels","border_around_the_chart","collapsible_header","constraint_dialogs_movement","context_menus","control_bar","create_volume_indicator_by_default","custom_items_in_context_menu","datasource_copypaste","uppercase_instrument_names","display_market_status","edit_buttons_in_legend","object_tree_legend_mode","graying_disabled_tools_enabled","header_widget","legend_widget","header_saveload","dont_show_boolean_study_arguments","lean_chart_load","left_toolbar","right_toolbar","link_to_tradingview","pay_attention_to_ticker_not_symbol","plain_studymarket","refresh_saved_charts_list_on_dialog_show","right_bar_stays_on_scroll","saveload_storage_customization","stop_study_on_restart","timeframes_toolbar","symbol_search_hot_key","update_study_formatter_on_symbol_resolve","update_timeframes_set_on_symbol_resolve","use_localstorage_for_settings","volume_force_overlay","widget_logo","countdown","use_overrides_for_overlay","trading_notifications","compare_symbol","symbol_info","timezone_menu","main_series_scale_menu","create_volume_indicator_by_default_once","bars_marks","chart_scroll","chart_zoom","source_selection_markers","property_pages","go_to_date","adaptive_logo","show_animated_logo","handle_scale","handle_scroll","shift_visible_range_on_new_bar","chart_content_overrides_by_defaults","cropped_tick_marks","scales_date_format","scales_time_hours_format","popup_hints","save_shortcut","show_right_widgets_panel_by_default","show_object_tree","insert_indicator_dialog_shortcut","compare_recent_symbols_enabled","hide_main_series_symbol_from_indicator_legend","chart_style_hilo","request_only_visible_range_on_reset","clear_price_scale_on_error_or_empty_bars","show_symbol_logo_in_legend","show_symbol_logo_for_compare_studies","library_custom_color_themes"]},"charting_library":{"subsets":["charting_library_base"]},"static_charts_service":{"subsets":["charting_library","disable_resolution_rebuild"]},"trading_terminal":{"subsets":["charting_library_base","support_multicharts","header_layouttoggle","japanese_chart_styles","chart_property_page_trading","add_to_watchlist","open_account_manager","show_dom_first_time","order_panel","buy_sell_buttons","multiple_watchlists","show_trading_notifications_history","always_pass_called_order_to_modify","show_object_tree","watchlist_import_export","drawing_templates","trading_account_manager","chart_crosshair_menu","compare_recent_symbols_enabled","watchlist_context_menu","show_symbol_logo_in_account_manager","watchlist_sections","prefer_quote_short_name","enable_dom_data_for_untradable_symbols","prefer_symbol_name_over_fullname","watchlist_cross_tab_sync"]}}') +;var o=n.t(r,2);const i=new Map,a=new Map,s=new Set;function u(e){const t=i.get(e);if(void 0!==t)return t;const n=a.get(e);return!!n&&n.some(u)}function c(e,t){i.set(String(e),Boolean(t))}function l(e){c(e,!0)}function f(e){c(e,!1)}function d(){const e=Object.create(null);for(const t of s)e[t]=u(t);return e}!function(){for(const[e,t]of Object.entries(o))if(s.add(e),"subsets"in t)for(const n of t.subsets){s.add(n);let t=a.get(n);void 0===t&&(t=[],a.set(n,t)),t.push(e)}"object"==typeof __initialDisabledFeaturesets&&Array.isArray(__initialDisabledFeaturesets)&&__initialDisabledFeaturesets.forEach(f),"object"==typeof __initialEnabledFeaturesets&&Array.isArray(__initialEnabledFeaturesets)&&__initialEnabledFeaturesets.forEach(l)}()},37265:function(e,t,n){e=n.nmd(e);const{clone:r,merge:o,isFunction:i,deepEquals:a,isObject:s,isNumber:u}=n(97085);var c,l=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)};function f(e,t){e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}"undefined"!=typeof window?(c=window.TradingView=window.TradingView||{},window.isNumber=u,window.isFunction=i,window.inherit=f,window.isArray=l):c=this.TradingView=this.TradingView||{},c.isNaN=function(e){return!(e<=0||e>0)},c.isAbsent=function(e){return null==e},c.isExistent=function(e){return null!=e},Number.isNaN=Number.isNaN||function(e){return e!=e},c.isSameType=function(e,t){return Number.isNaN(e)||Number.isNaN(t)?Number.isNaN(e)===Number.isNaN(t):{}.toString.call(e)==={}.toString.call(t)},c.isInteger=function(e){return"number"==typeof e&&e%1==0},c.isString=function(e){return null!=e&&e.constructor===String},c.isInherited=function(e,t){if(null==e||null==e.prototype)throw new TypeError("isInherited: child should be a constructor function");if(null==t||null==t.prototype)throw new TypeError("isInherited: parent should be a constructor function");return e.prototype instanceof t||e.prototype===t.prototype},c.clone=r,c.deepEquals=a,c.merge=o,e&&e.exports&&(e.exports={inherit:f,clone:c.clone,merge:c.merge,isNumber:u,isInteger:c.isInteger,isString:c.isString,isObject:s,isHashObject:function(e){return s(e)&&-1!==e.constructor.toString().indexOf("function Object")},isPromise:function(e){return s(e)&&e.then},isNaN:c.isNaN,isAbsent:c.isAbsent,isExistent:c.isExistent,isSameType:c.isSameType,isArray:l,isFunction:i,parseBool:c.parseBool,deepEquals:a,notNull:function(e){return null!==e},notUndefined:function(e){return void 0!==e},isEven:function(e){return e%2==0},declareClassAsPureInterface:function(e,t){for(var n in e.prototype)"function"==typeof e.prototype[n]&&e.prototype.hasOwnProperty(n)&&(e.prototype[n]=function(){throw new Error(t+"::"+n+" is an interface member declaration and must be overloaded in order to be called")})},requireFullInterfaceImplementation:function(e,t,n,r){for(var o in n.prototype)if("function"==typeof n.prototype[o]&&!e.prototype[o])throw new Error("Interface implementation assertion failed: "+t+" does not implement "+r+"::"+o+" function")}})}, +21251:(e,t,n)=>{"use strict";n.r(t);var r=n(37265);const o=/{(\w+)}/g,i=/{(\d+)}/g;String.prototype.format=function(...e){const t=(0,r.isObject)(e[0]),n=t?o:i,a=t?(t,n)=>{const r=e[0];return void 0!==r[n]?r[n]:t}:(t,n)=>{const r=parseInt(n,10),o=e[r];return void 0!==o?o:t};return this.replace(n,a)}},44286:()=>{"use strict";var e,t,n,r,o,i;window.parent!==window&&window.CanvasRenderingContext2D&&window.TextMetrics&&(t=window.CanvasRenderingContext2D.prototype)&&t.hasOwnProperty("font")&&t.hasOwnProperty("mozTextStyle")&&"function"==typeof t.__lookupSetter__&&(n=t.__lookupSetter__("font"))&&(t.__defineSetter__("font",(function(e){try{return n.call(this,e)}catch(e){if("NS_ERROR_FAILURE"!==e.name)throw e}})),r=t.measureText,e=function(){this.width=0,this.isFake=!0,this.__proto__=window.TextMetrics.prototype},t.measureText=function(t){try{return r.apply(this,arguments)}catch(t){if("NS_ERROR_FAILURE"!==t.name)throw t;return new e}},o=t.fillText,t.fillText=function(e,t,n,r){try{o.apply(this,arguments)}catch(e){if("NS_ERROR_FAILURE"!==e.name)throw e}},i=t.strokeText,t.strokeText=function(e,t,n,r){try{i.apply(this,arguments)}catch(e){if("NS_ERROR_FAILURE"!==e.name)throw e}})},85459:function(e,t,n){var r;!function(t){"use strict";function o(){}var i=o.prototype,a=t.EventEmitter;function s(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function u(e){return function(){return this[e].apply(this,arguments)}}function c(e){return"function"==typeof e||e instanceof RegExp||!(!e||"object"!=typeof e)&&c(e.listener)}i.getListeners=function(e){var t,n,r=this._getEvents();if(e instanceof RegExp)for(n in t={},r)r.hasOwnProperty(n)&&e.test(n)&&(t[n]=r[n]);else t=r[e]||(r[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;t{"use strict";function r(e){var t=e.width,n=e.height;if(t<0)throw new Error("Negative width is not allowed for Size");if(n<0)throw new Error("Negative height is not allowed for Size");return{width:t,height:n}}function o(e,t){return e.width===t.width&&e.height===t.height}n.d(t,{CanvasRenderingTarget2D:()=>c,bindCanvasElementBitmapSizeTo:()=>s,equalSizes:()=>o,size:()=>r});var i=function(){function e(e){var t=this;this._resolutionListener=function(){return t._onResolutionChanged()},this._resolutionMediaQueryList=null,this._observers=[],this._window=e,this._installResolutionListener()}return e.prototype.dispose=function(){this._uninstallResolutionListener(),this._window=null},Object.defineProperty(e.prototype,"value",{get:function(){return this._window.devicePixelRatio},enumerable:!1,configurable:!0}),e.prototype.subscribe=function(e){var t=this,n={next:e};return this._observers.push(n),{unsubscribe:function(){t._observers=t._observers.filter((function(e){return e!==n}))}}},e.prototype._installResolutionListener=function(){if(null!==this._resolutionMediaQueryList)throw new Error("Resolution listener is already installed");var e=this._window.devicePixelRatio;this._resolutionMediaQueryList=this._window.matchMedia("all and (resolution: ".concat(e,"dppx)")),this._resolutionMediaQueryList.addListener(this._resolutionListener)},e.prototype._uninstallResolutionListener=function(){null!==this._resolutionMediaQueryList&&(this._resolutionMediaQueryList.removeListener(this._resolutionListener),this._resolutionMediaQueryList=null)},e.prototype._reinstallResolutionListener=function(){this._uninstallResolutionListener(),this._installResolutionListener()},e.prototype._onResolutionChanged=function(){var e=this;this._observers.forEach((function(t){return t.next(e._window.devicePixelRatio)})), +this._reinstallResolutionListener()},e}();var a=function(){function e(e,t,n){var o;this._canvasElement=null,this._bitmapSizeChangedListeners=[],this._suggestedBitmapSize=null,this._suggestedBitmapSizeChangedListeners=[],this._devicePixelRatioObservable=null,this._canvasElementResizeObserver=null,this._canvasElement=e,this._canvasElementClientSize=r({width:this._canvasElement.clientWidth,height:this._canvasElement.clientHeight}),this._transformBitmapSize=null!=t?t:function(e){return e},this._allowResizeObserver=null===(o=null==n?void 0:n.allowResizeObserver)||void 0===o||o,this._chooseAndInitObserver()}return e.prototype.dispose=function(){var e,t;if(null===this._canvasElement)throw new Error("Object is disposed");null===(e=this._canvasElementResizeObserver)||void 0===e||e.disconnect(),this._canvasElementResizeObserver=null,null===(t=this._devicePixelRatioObservable)||void 0===t||t.dispose(),this._devicePixelRatioObservable=null,this._suggestedBitmapSizeChangedListeners.length=0,this._bitmapSizeChangedListeners.length=0,this._canvasElement=null},Object.defineProperty(e.prototype,"canvasElement",{get:function(){if(null===this._canvasElement)throw new Error("Object is disposed");return this._canvasElement},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"canvasElementClientSize",{get:function(){return this._canvasElementClientSize},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"bitmapSize",{get:function(){return r({width:this.canvasElement.width,height:this.canvasElement.height})},enumerable:!1,configurable:!0}),e.prototype.resizeCanvasElement=function(e){this._canvasElementClientSize=r(e),this.canvasElement.style.width="".concat(this._canvasElementClientSize.width,"px"),this.canvasElement.style.height="".concat(this._canvasElementClientSize.height,"px"),this._invalidateBitmapSize()},e.prototype.subscribeBitmapSizeChanged=function(e){this._bitmapSizeChangedListeners.push(e)},e.prototype.unsubscribeBitmapSizeChanged=function(e){this._bitmapSizeChangedListeners=this._bitmapSizeChangedListeners.filter((function(t){return t!==e}))},Object.defineProperty(e.prototype,"suggestedBitmapSize",{get:function(){return this._suggestedBitmapSize},enumerable:!1,configurable:!0}),e.prototype.subscribeSuggestedBitmapSizeChanged=function(e){this._suggestedBitmapSizeChangedListeners.push(e)},e.prototype.unsubscribeSuggestedBitmapSizeChanged=function(e){this._suggestedBitmapSizeChangedListeners=this._suggestedBitmapSizeChangedListeners.filter((function(t){return t!==e}))},e.prototype.applySuggestedBitmapSize=function(){if(null!==this._suggestedBitmapSize){var e=this._suggestedBitmapSize;this._suggestedBitmapSize=null,this._resizeBitmap(e),this._emitSuggestedBitmapSizeChanged(e,this._suggestedBitmapSize)}},e.prototype._resizeBitmap=function(e){var t=this.bitmapSize;o(t,e)||(this.canvasElement.width=e.width,this.canvasElement.height=e.height,this._emitBitmapSizeChanged(t,e))},e.prototype._emitBitmapSizeChanged=function(e,t){var n=this;this._bitmapSizeChangedListeners.forEach((function(r){ +return r.call(n,e,t)}))},e.prototype._suggestNewBitmapSize=function(e){var t=this._suggestedBitmapSize,n=r(this._transformBitmapSize(e,this._canvasElementClientSize)),i=o(this.bitmapSize,n)?null:n;null===t&&null===i||null!==t&&null!==i&&o(t,i)||(this._suggestedBitmapSize=i,this._emitSuggestedBitmapSizeChanged(t,i))},e.prototype._emitSuggestedBitmapSizeChanged=function(e,t){var n=this;this._suggestedBitmapSizeChangedListeners.forEach((function(r){return r.call(n,e,t)}))},e.prototype._chooseAndInitObserver=function(){var e=this;this._allowResizeObserver?new Promise((function(e){var t=new ResizeObserver((function(n){e(n.every((function(e){return"devicePixelContentBoxSize"in e}))),t.disconnect()}));t.observe(document.body,{box:"device-pixel-content-box"})})).catch((function(){return!1})).then((function(t){return t?e._initResizeObserver():e._initDevicePixelRatioObservable()})):this._initDevicePixelRatioObservable()},e.prototype._initDevicePixelRatioObservable=function(){var e=this;if(null!==this._canvasElement){var t=u(this._canvasElement);if(null===t)throw new Error("No window is associated with the canvas");this._devicePixelRatioObservable=function(e){return new i(e)}(t),this._devicePixelRatioObservable.subscribe((function(){return e._invalidateBitmapSize()})),this._invalidateBitmapSize()}},e.prototype._invalidateBitmapSize=function(){var e,t;if(null!==this._canvasElement){var n=u(this._canvasElement);if(null!==n){var o=null!==(t=null===(e=this._devicePixelRatioObservable)||void 0===e?void 0:e.value)&&void 0!==t?t:n.devicePixelRatio,i=this._canvasElement.getClientRects(),a=void 0!==i[0]?function(e,t){return r({width:Math.round(e.left*t+e.width*t)-Math.round(e.left*t),height:Math.round(e.top*t+e.height*t)-Math.round(e.top*t)})}(i[0],o):r({width:this._canvasElementClientSize.width*o,height:this._canvasElementClientSize.height*o});this._suggestNewBitmapSize(a)}}},e.prototype._initResizeObserver=function(){var e=this;null!==this._canvasElement&&(this._canvasElementResizeObserver=new ResizeObserver((function(t){var n=t.find((function(t){return t.target===e._canvasElement}));if(n&&n.devicePixelContentBoxSize&&n.devicePixelContentBoxSize[0]){var o=n.devicePixelContentBoxSize[0],i=r({width:o.inlineSize,height:o.blockSize});e._suggestNewBitmapSize(i)}})),this._canvasElementResizeObserver.observe(this._canvasElement,{box:"device-pixel-content-box"}))},e}();function s(e,t){if("device-pixel-content-box"===t.type)return new a(e,t.transform,t.options);throw new Error("Unsupported binding target")}function u(e){return e.ownerDocument.defaultView}var c=function(){function e(e,t,n){if(0===t.width||0===t.height)throw new TypeError("Rendering target could only be created on a media with positive width and height");if(this._mediaSize=t,0===n.width||0===n.height)throw new TypeError("Rendering target could only be created using a bitmap with positive integer width and height");this._bitmapSize=n,this._context=e}return e.prototype.useMediaCoordinateSpace=function(e){try{return this._context.save(),this._context.setTransform(1,0,0,1,0,0), +this._context.scale(this._horizontalPixelRatio,this._verticalPixelRatio),e({context:this._context,mediaSize:this._mediaSize})}finally{this._context.restore()}},e.prototype.useBitmapCoordinateSpace=function(e){try{return this._context.save(),this._context.setTransform(1,0,0,1,0,0),e({context:this._context,mediaSize:this._mediaSize,bitmapSize:this._bitmapSize,horizontalPixelRatio:this._horizontalPixelRatio,verticalPixelRatio:this._verticalPixelRatio})}finally{this._context.restore()}},Object.defineProperty(e.prototype,"_horizontalPixelRatio",{get:function(){return this._bitmapSize.width/this._mediaSize.width},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"_verticalPixelRatio",{get:function(){return this._bitmapSize.height/this._mediaSize.height},enumerable:!1,configurable:!0}),e}()},46956:(e,t,n)=>{"use strict";n.d(t,{default:()=>d});const r=function(){this.__data__=[],this.size=0};var o=n(54523);const i=function(e,t){for(var n=e.length;n--;)if((0,o.default)(e[n][0],t))return n;return-1};var a=Array.prototype.splice;const s=function(e){var t=this.__data__,n=i(t,e);return!(n<0)&&(n==t.length-1?t.pop():a.call(t,n,1),--this.size,!0)};const u=function(e){var t=this.__data__,n=i(t,e);return n<0?void 0:t[n][1]};const c=function(e){return i(this.__data__,e)>-1};const l=function(e,t){var n=this.__data__,r=i(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this};function f(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t{"use strict";n.d(t,{default:()=>i});var r=n(52494),o=n(99615);const i=(0,r.default)(o.default,"Map")},75440:(e,t,n)=>{"use strict";n.d(t,{default:()=>j});const r=(0,n(52494).default)(Object,"create");const o=function(){this.__data__=r?r(null):{},this.size=0};const i=function(e){var t=this.has(e)&&delete this.__data__[e];return this.size-=t?1:0,t};var a=Object.prototype.hasOwnProperty;const s=function(e){var t=this.__data__;if(r){var n=t[e];return"__lodash_hash_undefined__"===n?void 0:n}return a.call(t,e)?t[e]:void 0};var u=Object.prototype.hasOwnProperty;const c=function(e){var t=this.__data__;return r?void 0!==t[e]:u.call(t,e)};const l=function(e,t){var n=this.__data__;return this.size+=this.has(e)?0:1,n[e]=r&&void 0===t?"__lodash_hash_undefined__":t,this};function f(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t{"use strict";n.d(t,{default:()=>d});var r=n(46956);const o=function(){this.__data__=new r.default,this.size=0};const i=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n};const a=function(e){return this.__data__.get(e)};const s=function(e){return this.__data__.has(e)};var u=n(19385),c=n(75440);const l=function(e,t){var n=this.__data__;if(n instanceof r.default){var o=n.__data__;if(!u.default||o.length<199)return o.push([e,t]),this.size=++n.size,this;n=this.__data__=new c.default(o)}return n.set(e,t),this.size=n.size,this};function f(e){var t=this.__data__=new r.default(e);this.size=t.size}f.prototype.clear=o,f.prototype.delete=i,f.prototype.get=a,f.prototype.has=s,f.prototype.set=l;const d=f},66711:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=n(99615).default.Symbol},16299:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=n(99615).default.Uint8Array},60545:(e,t,n)=>{"use strict";n.d(t,{default:()=>l});const r=function(e,t){for(var n=-1,r=Array(e);++n{"use strict";n.d(t,{default:()=>r});const r=function(e,t){for(var n=-1,r=t.length,o=e.length;++n{"use strict";n.d(t,{default:()=>a});var r=n(857),o=n(54523),i=Object.prototype.hasOwnProperty;const a=function(e,t,n){var a=e[t];i.call(e,t)&&(0,o.default)(a,n)&&(void 0!==n||t in e)||(0,r.default)(e,t,n)}},857:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=n(55136);const o=function(e,t,n){"__proto__"==t&&r.default?(0,r.default)(e,t,{configurable:!0,enumerable:!0,value:n,writable:!0}):e[t]=n}},76507:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e){return function(t,n,r){for(var o=-1,i=Object(t),a=r(t),s=a.length;s--;){var u=a[e?s:++o];if(!1===n(i[u],u,i))break}return t}}()},49084:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(31434),o=n(13383);const i=function(e,t){for(var n=0,i=(t=(0,r.default)(t,e)).length;null!=e&&n{"use strict";n.d(t,{default:()=>i});var r=n(18573),o=n(56052);const i=function(e,t,n){var i=t(e);return(0,o.default)(e)?i:(0,r.default)(i,n(e))}},89572:(e,t,n)=>{"use strict";n.d(t,{default:()=>d}) +;var r=n(66711),o=Object.prototype,i=o.hasOwnProperty,a=o.toString,s=r.default?r.default.toStringTag:void 0;const u=function(e){var t=i.call(e,s),n=e[s];try{e[s]=void 0;var r=!0}catch(e){}var o=a.call(e);return r&&(t?e[s]=n:delete e[s]),o};var c=Object.prototype.toString;const l=function(e){return c.call(e)};var f=r.default?r.default.toStringTag:void 0;const d=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":f&&f in Object(e)?u(e):l(e)}},12189:(e,t,n)=>{"use strict";n.d(t,{default:()=>C});var r=n(87593),o=n(75440);const i=function(e){return this.__data__.set(e,"__lodash_hash_undefined__"),this};const a=function(e){return this.__data__.has(e)};function s(e){var t=-1,n=null==e?0:e.length;for(this.__data__=new o.default;++ts))return!1;var d=i.get(e),_=i.get(t);if(d&&_)return d==t&&_==e;var h=-1,p=!0,b=2&n?new u:void 0;for(i.set(e,t),i.set(t,e);++h{"use strict";n.d(t,{default:()=>a});var r=n(5196);const o=(0,n(45635).default)(Object.keys,Object);var i=Object.prototype.hasOwnProperty;const a=function(e){if(!(0,r.default)(e))return o(e);var t=[];for(var n in Object(e))i.call(e,n)&&"constructor"!=n&&t.push(n);return t}},38459:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e,t,n){var r=-1,o=e.length;t<0&&(t=-t>o?0:o+t),(n=n>o?o:n)<0&&(n+=o),o=t>n?0:n-t>>>0,t>>>=0;for(var i=Array(o);++r{"use strict";n.d(t,{default:()=>r});const r=function(e){return function(t){return e(t)}}},31434:(e,t,n)=>{"use strict";n.d(t,{default:()=>b});var r=n(56052),o=n(61070),i=n(59332);var a=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,s=/\\(\\)?/g;const u=function(e){var t=(0,i.default)(e,(function(e){return 500===n.size&&n.clear(),e})),n=t.cache;return t}((function(e){var t=[];return 46===e.charCodeAt(0)&&t.push(""),e.replace(a,(function(e,n,r,o){t.push(r?o.replace(s,"$1"):n||e)})),t}));var c=n(66711);const l=function(e,t){for(var n=-1,r=null==e?0:e.length,o=Array(r);++n{"use strict";n.d(t,{default:()=>o});var r=n(16299);const o=function(e){var t=new e.constructor(e.byteLength);return new r.default(t).set(new r.default(e)),t}},14054:(e,t,n)=>{"use strict";n.d(t,{default:()=>u});var r=n(99615),o="object"==typeof exports&&exports&&!exports.nodeType&&exports,i=o&&"object"==typeof module&&module&&!module.nodeType&&module,a=i&&i.exports===o?r.default.Buffer:void 0,s=a?a.allocUnsafe:void 0;const u=function(e,t){if(t)return e.slice();var n=e.length,r=s?s(n):new e.constructor(n);return e.copy(r),r} +},11523:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=n(22605);const o=function(e,t){var n=t?(0,r.default)(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.length)}},32126:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e,t){var n=-1,r=e.length;for(t||(t=Array(r));++n{"use strict";n.d(t,{default:()=>i});var r=n(61572),o=n(857);const i=function(e,t,n,i){var a=!n;n||(n={});for(var s=-1,u=t.length;++s{"use strict";n.d(t,{default:()=>o});var r=n(52494);const o=function(){try{var e=(0,r.default)(Object,"defineProperty");return e({},"",{}),e}catch(e){}}()},97889:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r="object"==typeof global&&global&&global.Object===Object&&global},38366:(e,t,n)=>{"use strict";n.d(t,{default:()=>a});var r=n(96909),o=n(21578),i=n(77251);const a=function(e){return(0,r.default)(e,i.default,o.default)}},52494:(e,t,n)=>{"use strict";n.d(t,{default:()=>g});var r=n(88987);const o=n(99615).default["__core-js_shared__"];var i,a=(i=/[^.]+$/.exec(o&&o.keys&&o.keys.IE_PROTO||""))?"Symbol(src)_1."+i:"";const s=function(e){return!!a&&a in e};var u=n(82433),c=n(65114),l=/^\[object .+?Constructor\]$/,f=Function.prototype,d=Object.prototype,_=f.toString,h=d.hasOwnProperty,p=RegExp("^"+_.call(h).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");const b=function(e){return!(!(0,u.default)(e)||s(e))&&((0,r.default)(e)?p:l).test((0,c.default)(e))};const v=function(e,t){return null==e?void 0:e[t]};const g=function(e,t){var n=v(e,t);return b(n)?n:void 0}},10964:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=(0,n(45635).default)(Object.getPrototypeOf,Object)},21578:(e,t,n)=>{"use strict";n.d(t,{default:()=>s});const r=function(e,t){for(var n=-1,r=null==e?0:e.length,o=0,i=[];++n{"use strict";n.d(t,{default:()=>j});var r=n(52494),o=n(99615);const i=(0,r.default)(o.default,"DataView");var a=n(19385);const s=(0,r.default)(o.default,"Promise");const u=(0,r.default)(o.default,"Set");const c=(0,r.default)(o.default,"WeakMap");var l=n(89572),f=n(65114),d="[object Map]",_="[object Promise]",h="[object Set]",p="[object WeakMap]",b="[object DataView]",v=(0,f.default)(i),g=(0,f.default)(a.default),m=(0,f.default)(s),y=(0,f.default)(u),w=(0,f.default)(c),x=l.default;(i&&x(new i(new ArrayBuffer(1)))!=b||a.default&&x(new a.default)!=d||s&&x(s.resolve())!=_||u&&x(new u)!=h||c&&x(new c)!=p)&&(x=function(e){var t=(0,l.default)(e),n="[object Object]"==t?e.constructor:void 0,r=n?(0,f.default)(n):"";if(r)switch(r){case v:return b;case g:return d;case m:return _;case y:return h;case w:return p}return t}) +;const j=x},85146:(e,t,n)=>{"use strict";n.d(t,{default:()=>u});var r=n(82433),o=Object.create;const i=function(){function e(){}return function(t){if(!(0,r.default)(t))return{};if(o)return o(t);e.prototype=t;var n=new e;return e.prototype=void 0,n}}();var a=n(10964),s=n(5196);const u=function(e){return"function"!=typeof e.constructor||(0,s.default)(e)?{}:i((0,a.default)(e))}},99313:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=/^(?:0|[1-9]\d*)$/;const o=function(e,t){var n=typeof e;return!!(t=t??9007199254740991)&&("number"==n||"symbol"!=n&&r.test(e))&&e>-1&&e%1==0&&e{"use strict";n.d(t,{default:()=>s});var r=n(54523),o=n(49634),i=n(99313),a=n(82433);const s=function(e,t,n){if(!(0,a.default)(n))return!1;var s=typeof t;return!!("number"==s?(0,o.default)(n)&&(0,i.default)(t,n.length):"string"==s&&t in n)&&(0,r.default)(n[t],e)}},61070:(e,t,n)=>{"use strict";n.d(t,{default:()=>s});var r=n(56052),o=n(98111),i=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,a=/^\w*$/;const s=function(e,t){if((0,r.default)(e))return!1;var n=typeof e;return!("number"!=n&&"symbol"!=n&&"boolean"!=n&&null!=e&&!(0,o.default)(e))||(a.test(e)||!i.test(e)||null!=t&&e in Object(t))}},5196:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=Object.prototype;const o=function(e){var t=e&&e.constructor;return e===("function"==typeof t&&t.prototype||r)}},92350:(e,t,n)=>{"use strict";n.d(t,{default:()=>s});var r=n(97889),o="object"==typeof exports&&exports&&!exports.nodeType&&exports,i=o&&"object"==typeof module&&module&&!module.nodeType&&module,a=i&&i.exports===o&&r.default.process;const s=function(){try{var e=i&&i.require&&i.require("util").types;return e||a&&a.binding&&a.binding("util")}catch(e){}}()},45635:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e,t){return function(n){return e(t(n))}}},99615:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(97889),o="object"==typeof self&&self&&self.Object===Object&&self;const i=r.default||o||Function("return this")()},13383:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=n(98111);const o=function(e){if("string"==typeof e||(0,r.default)(e))return e;var t=e+"";return"0"==t&&1/e==-1/0?"-0":t}},65114:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=Function.prototype.toString;const o=function(e){if(null!=e){try{return r.call(e)}catch(e){}try{return e+""}catch(e){}}return""}},90054:(e,t,n)=>{"use strict";n.d(t,{default:()=>K});var r=n(87593);const o=function(e,t){for(var n=-1,r=null==e?0:e.length;++n{"use strict";n.d(t,{default:()=>c});var r=n(82433),o=n(99615);const i=function(){ +return o.default.Date.now()};var a=n(78677),s=Math.max,u=Math.min;const c=function(e,t,n){var o,c,l,f,d,_,h=0,p=!1,b=!1,v=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function g(t){var n=o,r=c;return o=c=void 0,h=t,f=e.apply(r,n)}function m(e){var n=e-_;return void 0===_||n>=t||n<0||b&&e-h>=l}function y(){var e=i();if(m(e))return w(e);d=setTimeout(y,function(e){var n=t-(e-_);return b?u(n,l-(e-h)):n}(e))}function w(e){return d=void 0,v&&o?g(e):(o=c=void 0,f)}function x(){var e=i(),n=m(e);if(o=arguments,c=this,_=e,n){if(void 0===d)return function(e){return h=e,d=setTimeout(y,t),p?g(e):f}(_);if(b)return clearTimeout(d),d=setTimeout(y,t),g(_)}return void 0===d&&(d=setTimeout(y,t)),f}return t=(0,a.default)(t)||0,(0,r.default)(n)&&(p=!!n.leading,l=(b="maxWait"in n)?s((0,a.default)(n.maxWait)||0,t):l,v="trailing"in n?!!n.trailing:v),x.cancel=function(){void 0!==d&&clearTimeout(d),h=0,o=_=c=d=void 0},x.flush=function(){return void 0===d?f:w(i())},x}},54523:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e,t){return e===t||e!=e&&t!=t}},54029:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=n(49084);const o=function(e,t,n){var o=null==e?void 0:(0,r.default)(e,t);return void 0===o?n:o}},76402:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e){return e}},54404:(e,t,n)=>{"use strict";n.d(t,{default:()=>c});var r=n(89572),o=n(13795);const i=function(e){return(0,o.default)(e)&&"[object Arguments]"==(0,r.default)(e)};var a=Object.prototype,s=a.hasOwnProperty,u=a.propertyIsEnumerable;const c=i(function(){return arguments}())?i:function(e){return(0,o.default)(e)&&s.call(e,"callee")&&!u.call(e,"callee")}},56052:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=Array.isArray},49634:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(88987),o=n(65743);const i=function(e){return null!=e&&(0,o.default)(e.length)&&!(0,r.default)(e)}},83350:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(89572),o=n(13795);const i=function(e){return!0===e||!1===e||(0,o.default)(e)&&"[object Boolean]"==(0,r.default)(e)}},32437:(e,t,n)=>{"use strict";n.d(t,{default:()=>u});var r=n(99615);const o=function(){return!1};var i="object"==typeof exports&&exports&&!exports.nodeType&&exports,a=i&&"object"==typeof module&&module&&!module.nodeType&&module,s=a&&a.exports===i?r.default.Buffer:void 0;const u=(s?s.isBuffer:void 0)||o},15943:(e,t,n)=>{"use strict";n.d(t,{default:()=>d});var r=n(89815),o=n(81296),i=n(54404),a=n(56052),s=n(49634),u=n(32437),c=n(5196),l=n(9125),f=Object.prototype.hasOwnProperty;const d=function(e){if(null==e)return!0;if((0,s.default)(e)&&((0,a.default)(e)||"string"==typeof e||"function"==typeof e.splice||(0,u.default)(e)||(0,l.default)(e)||(0,i.default)(e)))return!e.length;var t=(0,o.default)(e);if("[object Map]"==t||"[object Set]"==t)return!e.size;if((0,c.default)(e))return!(0,r.default)(e).length;for(var n in e)if(f.call(e,n))return!1;return!0}},50279:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=n(12189);const o=function(e,t){return(0,r.default)(e,t)}},88987:(e,t,n)=>{ +"use strict";n.d(t,{default:()=>i});var r=n(89572),o=n(82433);const i=function(e){if(!(0,o.default)(e))return!1;var t=(0,r.default)(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t}},65743:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=9007199254740991}},43888:(e,t,n)=>{"use strict";n.d(t,{default:()=>c});var r=n(81296),o=n(13795);const i=function(e){return(0,o.default)(e)&&"[object Map]"==(0,r.default)(e)};var a=n(5467),s=n(92350),u=s.default&&s.default.isMap;const c=u?(0,a.default)(u):i},63193:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e){return null==e}},69708:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(89572),o=n(13795);const i=function(e){return"number"==typeof e||(0,o.default)(e)&&"[object Number]"==(0,r.default)(e)}},82433:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},13795:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e){return null!=e&&"object"==typeof e}},83873:(e,t,n)=>{"use strict";n.d(t,{default:()=>a});var r=n(89572),o=n(56052),i=n(13795);const a=function(e){return"string"==typeof e||!(0,o.default)(e)&&(0,i.default)(e)&&"[object String]"==(0,r.default)(e)}},98111:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(89572),o=n(13795);const i=function(e){return"symbol"==typeof e||(0,o.default)(e)&&"[object Symbol]"==(0,r.default)(e)}},9125:(e,t,n)=>{"use strict";n.d(t,{default:()=>f});var r=n(89572),o=n(65743),i=n(13795),a={};a["[object Float32Array]"]=a["[object Float64Array]"]=a["[object Int8Array]"]=a["[object Int16Array]"]=a["[object Int32Array]"]=a["[object Uint8Array]"]=a["[object Uint8ClampedArray]"]=a["[object Uint16Array]"]=a["[object Uint32Array]"]=!0,a["[object Arguments]"]=a["[object Array]"]=a["[object ArrayBuffer]"]=a["[object Boolean]"]=a["[object DataView]"]=a["[object Date]"]=a["[object Error]"]=a["[object Function]"]=a["[object Map]"]=a["[object Number]"]=a["[object Object]"]=a["[object RegExp]"]=a["[object Set]"]=a["[object String]"]=a["[object WeakMap]"]=!1;const s=function(e){return(0,i.default)(e)&&(0,o.default)(e.length)&&!!a[(0,r.default)(e)]};var u=n(5467),c=n(92350),l=c.default&&c.default.isTypedArray;const f=l?(0,u.default)(l):s},77251:(e,t,n)=>{"use strict";n.d(t,{default:()=>a});var r=n(60545),o=n(89815),i=n(49634);const a=function(e){return(0,i.default)(e)?(0,r.default)(e):(0,o.default)(e)}},2960:(e,t,n)=>{"use strict";n.d(t,{default:()=>l});var r=n(60545),o=n(82433),i=n(5196);const a=function(e){var t=[];if(null!=e)for(var n in Object(e))t.push(n);return t};var s=Object.prototype.hasOwnProperty;const u=function(e){if(!(0,o.default)(e))return a(e);var t=(0,i.default)(e),n=[];for(var r in e)("constructor"!=r||!t&&s.call(e,r))&&n.push(r);return n};var c=n(49634);const l=function(e){return(0,c.default)(e)?(0,r.default)(e,!0):u(e)}},82593:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r=function(e){ +var t=null==e?0:e.length;return t?e[t-1]:void 0}},59332:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(75440);function o(e,t){if("function"!=typeof e||null!=t&&"function"!=typeof t)throw new TypeError("Expected a function");var n=function(){var r=arguments,o=t?t.apply(this,r):r[0],i=n.cache;if(i.has(o))return i.get(o);var a=e.apply(this,r);return n.cache=i.set(o,a)||i,a};return n.cache=new(o.Cache||r.default),n}o.Cache=r.default;const i=o},16738:(e,t,n)=>{"use strict";n.d(t,{default:()=>H});var r=n(87593),o=n(857),i=n(54523);const a=function(e,t,n){(void 0!==n&&!(0,i.default)(e[t],n)||void 0===n&&!(t in e))&&(0,o.default)(e,t,n)};var s=n(76507),u=n(14054),c=n(11523),l=n(32126),f=n(85146),d=n(54404),_=n(56052),h=n(49634),p=n(13795);const b=function(e){return(0,p.default)(e)&&(0,h.default)(e)};var v=n(32437),g=n(88987),m=n(82433),y=n(89572),w=n(10964),x=Function.prototype,j=Object.prototype,E=x.toString,S=j.hasOwnProperty,O=E.call(Object);const z=function(e){if(!(0,p.default)(e)||"[object Object]"!=(0,y.default)(e))return!1;var t=(0,w.default)(e);if(null===t)return!0;var n=S.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&E.call(n)==O};var A=n(9125);const P=function(e,t){if(("constructor"!==t||"function"!=typeof e[t])&&"__proto__"!=t)return e[t]};var k=n(52949),R=n(2960);const L=function(e){return(0,k.default)(e,(0,R.default)(e))};const C=function(e,t,n,r,o,i,s){var h=P(e,n),p=P(t,n),y=s.get(p);if(y)a(e,n,y);else{var w=i?i(h,p,n+"",e,t,s):void 0,x=void 0===w;if(x){var j=(0,_.default)(p),E=!j&&(0,v.default)(p),S=!j&&!E&&(0,A.default)(p);w=p,j||E||S?(0,_.default)(h)?w=h:b(h)?w=(0,l.default)(h):E?(x=!1,w=(0,u.default)(p,!0)):S?(x=!1,w=(0,c.default)(p,!0)):w=[]:z(p)||(0,d.default)(p)?(w=h,(0,d.default)(h)?w=L(h):(0,m.default)(h)&&!(0,g.default)(h)||(w=(0,f.default)(p))):x=!1}x&&(s.set(p,w),o(w,p,r,i,s),s.delete(p)),a(e,n,w)}};const N=function e(t,n,o,i,u){t!==n&&(0,s.default)(n,(function(s,c){if(u||(u=new r.default),(0,m.default)(s))C(t,n,c,o,e,i,u);else{var l=i?i(P(t,c),s,c+"",t,n,u):void 0;void 0===l&&(l=s),a(t,c,l)}}),R.default)};var B=n(76402);const T=function(e,t,n){switch(n.length){case 0:return e.call(t);case 1:return e.call(t,n[0]);case 2:return e.call(t,n[0],n[1]);case 3:return e.call(t,n[0],n[1],n[2])}return e.apply(t,n)};var M=Math.max;const I=function(e,t,n){return t=M(void 0===t?e.length-1:t,0),function(){for(var r=arguments,o=-1,i=M(r.length-t,0),a=Array(i);++o0){if(++t>=800)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}($);const U=function(e,t){return V(I(e,t,B.default),e+"")};var W=n(61833);const H=function(e){return U((function(t,n){ +var r=-1,o=n.length,i=o>1?n[o-1]:void 0,a=o>2?n[2]:void 0;for(i=e.length>3&&"function"==typeof i?(o--,i):void 0,a&&(0,W.default)(n[0],n[1],a)&&(i=o<3?void 0:i,o=1),t=Object(t);++r{"use strict";n.d(t,{default:()=>i});var r=n(56882);const o=function(e,t){var n;if("function"!=typeof t)throw new TypeError("Expected a function");return e=(0,r.default)(e),function(){return--e>0&&(n=t.apply(this,arguments)),e<=1&&(t=void 0),n}};const i=function(e){return o(2,e)}},39852:(e,t,n)=>{"use strict";n.d(t,{default:()=>T});const r=function(e,t,n,r){var o=-1,i=null==e?0:e.length;for(r&&i&&(n=e[++o]);++o{"use strict";n.d(t,{default:()=>c});var r=n(61572),o=n(31434),i=n(99313),a=n(82433),s=n(13383) +;const u=function(e,t,n,u){if(!(0,a.default)(e))return e;for(var c=-1,l=(t=(0,o.default)(t,e)).length,f=l-1,d=e;null!=d&&++c{"use strict";n.d(t,{default:()=>r});const r=function(){return[]}},20057:(e,t,n)=>{"use strict";n.d(t,{default:()=>i});var r=n(90484),o=n(82433);const i=function(e,t,n){var i=!0,a=!0;if("function"!=typeof e)throw new TypeError("Expected a function");return(0,o.default)(n)&&(i="leading"in n?!!n.leading:i,a="trailing"in n?!!n.trailing:a),(0,r.default)(e,t,{leading:i,maxWait:t,trailing:a})}},56882:(e,t,n)=>{"use strict";n.d(t,{default:()=>a});var r=n(78677),o=1/0;const i=function(e){return e?(e=(0,r.default)(e))===o||e===-1/0?17976931348623157e292*(e<0?-1:1):e==e?e:0:0===e?e:0};const a=function(e){var t=i(e),n=t%1;return t==t?n?t-n:t:0}},78677:(e,t,n)=>{"use strict";n.d(t,{default:()=>_});var r=/\s/;const o=function(e){for(var t=e.length;t--&&r.test(e.charAt(t)););return t};var i=/^\s+/;const a=function(e){return e?e.slice(0,o(e)+1).replace(i,""):e};var s=n(82433),u=n(98111),c=/^[-+]0x[0-9a-f]+$/i,l=/^0b[01]+$/i,f=/^0o[0-7]+$/i,d=parseInt;const _=function(e){if("number"==typeof e)return e;if((0,u.default)(e))return NaN;if((0,s.default)(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=(0,s.default)(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=a(e);var n=l.test(e);return n||f.test(e)?d(e.slice(2),n?2:8):c.test(e)?NaN:+e}},81960:(e,t,n)=>{"use strict";n.d(t,{default:()=>l});var r=n(31434),o=n(82593),i=n(49084),a=n(38459);const s=function(e,t){return t.length<2?e:(0,i.default)(e,(0,a.default)(t,0,-1))};var u=n(13383);const c=function(e,t){return t=(0,r.default)(t,e),null==(e=s(e,t))||delete e[(0,u.default)((0,o.default)(t))]};const l=function(e,t){return null==e||c(e,t)}}}]); \ No newline at end of file diff --git a/frontend/charting_library/bundles/2157.61a636dade5d0e13d9e8.js b/frontend/charting_library/bundles/2157.61a636dade5d0e13d9e8.js new file mode 100644 index 0000000..4377f71 --- /dev/null +++ b/frontend/charting_library/bundles/2157.61a636dade5d0e13d9e8.js @@ -0,0 +1,11 @@ +(self.webpackChunktradingview=self.webpackChunktradingview||[]).push([[2157],{59142:function(e,t){var n,o,s;o=[t],n=function(e){"use strict";function t(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t{e.exports={wrapper:"wrapper-GZajBGIm",input:"input-GZajBGIm",view:"view-GZajBGIm",danger:"danger-GZajBGIm"}},4052:e=>{e.exports={box:"box-ywH2tsV_",noOutline:"noOutline-ywH2tsV_", +disabled:"disabled-ywH2tsV_","intent-danger":"intent-danger-ywH2tsV_",checked:"checked-ywH2tsV_",check:"check-ywH2tsV_",icon:"icon-ywH2tsV_",dot:"dot-ywH2tsV_",disableActiveStyles:"disableActiveStyles-ywH2tsV_"}},4665:e=>{e.exports={loader:"loader-UL6iwcBa",static:"static-UL6iwcBa",item:"item-UL6iwcBa","tv-button-loader":"tv-button-loader-UL6iwcBa",medium:"medium-UL6iwcBa",small:"small-UL6iwcBa",black:"black-UL6iwcBa",white:"white-UL6iwcBa",gray:"gray-UL6iwcBa",primary:"primary-UL6iwcBa"}},36110:e=>{e.exports={accessible:"accessible-rm8yeqY4"}},22846:e=>{e.exports={loaderWrap:"loaderWrap-jGEARQlM",big:"big-jGEARQlM",loader:"loader-jGEARQlM"}},31668:e=>{e.exports={item:"item-GJX1EXhk",interactive:"interactive-GJX1EXhk",hovered:"hovered-GJX1EXhk",disabled:"disabled-GJX1EXhk",active:"active-GJX1EXhk",shortcut:"shortcut-GJX1EXhk",normal:"normal-GJX1EXhk",big:"big-GJX1EXhk",iconCell:"iconCell-GJX1EXhk",icon:"icon-GJX1EXhk",content:"content-GJX1EXhk",label:"label-GJX1EXhk",checked:"checked-GJX1EXhk",toolbox:"toolbox-GJX1EXhk",showToolboxOnHover:"showToolboxOnHover-GJX1EXhk",arrowIcon:"arrowIcon-GJX1EXhk",subMenu:"subMenu-GJX1EXhk",invisibleHotkey:"invisibleHotkey-GJX1EXhk"}},21320:e=>{e.exports={row:"row-DFIg7eOh",line:"line-DFIg7eOh",hint:"hint-DFIg7eOh"}},37399:e=>{e.exports={menu:"menu-Tx5xMZww"}},70159:e=>{e.exports={item:"item-WJDah4zD",emptyIcons:"emptyIcons-WJDah4zD",loading:"loading-WJDah4zD",disabled:"disabled-WJDah4zD",interactive:"interactive-WJDah4zD",hovered:"hovered-WJDah4zD",normal:"normal-WJDah4zD",big:"big-WJDah4zD",icon:"icon-WJDah4zD",label:"label-WJDah4zD",title:"title-WJDah4zD",nested:"nested-WJDah4zD",shortcut:"shortcut-WJDah4zD",remove:"remove-WJDah4zD"}},20669:e=>{e.exports={separator:"separator-Ymxd0dt_"}},36718:e=>{e.exports={"default-drawer-min-top-distance":"100px",wrap:"wrap-_HnK0UIN",positionBottom:"positionBottom-_HnK0UIN",backdrop:"backdrop-_HnK0UIN",drawer:"drawer-_HnK0UIN",positionLeft:"positionLeft-_HnK0UIN"}},22413:e=>{e.exports={favorite:"favorite-_FRQhM5Y",hovered:"hovered-_FRQhM5Y",disabled:"disabled-_FRQhM5Y",focused:"focused-_FRQhM5Y",active:"active-_FRQhM5Y",checked:"checked-_FRQhM5Y"}},35990:e=>{e.exports={button:"button-iLKiGOdQ",hovered:"hovered-iLKiGOdQ",disabled:"disabled-iLKiGOdQ",focused:"focused-iLKiGOdQ",active:"active-iLKiGOdQ",hidden:"hidden-iLKiGOdQ"}},70673:(e,t,n)=>{"use strict";n.d(t,{CheckboxInput:()=>c});var o=n(50959),s=n(97754),r=n(90186),a=n(5811),i=n(11362),l=n.n(i);function c(e){const t=s(l().wrapper,e.className);return o.createElement("span",{className:t,title:e.title,style:e.style},o.createElement("input",{id:e.id,tabIndex:e.tabIndex,className:s(e.intent&&l()[e.intent],l().input),type:"checkbox",name:e.name,checked:e.checked,disabled:e.disabled,value:e.value,autoFocus:e.autoFocus,role:e.role,onChange:function(){e.onChange?.(e.value)},ref:e.reference,"aria-required":e["aria-required"],"aria-describedby":e["aria-describedby"],"aria-invalid":e["aria-invalid"],...(0,r.filterDataProps)(e)}),o.createElement(a.CheckboxView,{className:l().view, +indeterminate:e.indeterminate,checked:e.checked,disabled:e.disabled,intent:e.intent,tabIndex:e.tabIndex}))}},5811:(e,t,n)=>{"use strict";n.d(t,{CheckboxView:()=>c});var o=n(50959),s=n(97754),r=n(9745),a=n(65890),i=n(4052),l=n.n(i);function c(e){const{indeterminate:t,checked:n,tabIndex:i,className:c,disabled:u,disableActiveStyles:d,intent:h,hideIcon:m,...p}=e,v=t||!n||m?"":a,b=s(l().box,l()[`intent-${h}`],!t&&l().check,!!t&&l().dot,-1===i&&l().noOutline,c,n&&l().checked,u&&l().disabled,d&&l().disableActiveStyles);return o.createElement("span",{className:b,...p},o.createElement(r.Icon,{icon:v,className:l().icon}))}},26996:(e,t,n)=>{"use strict";n.d(t,{Loader:()=>l});var o,s=n(50959),r=n(97754),a=n(4665),i=n.n(a);function l(e){const{className:t,size:n="medium",staticPosition:o,color:a="black"}=e,l=r(i().item,i()[a],i()[n]);return s.createElement("span",{className:r(i().loader,o&&i().static,t)},s.createElement("span",{className:l}),s.createElement("span",{className:l}),s.createElement("span",{className:l}))}!function(e){e.Medium="medium",e.Small="small"}(o||(o={}))},74670:(e,t,n)=>{"use strict";n.d(t,{useActiveDescendant:()=>r});var o=n(50959),s=n(39416);function r(e,t=[]){const[n,r]=(0,o.useState)(!1),a=(0,s.useFunctionalRefObject)(e);return(0,o.useLayoutEffect)((()=>{const e=a.current;if(null===e)return;const t=e=>{switch(e.type){case"active-descendant-focus":r(!0);break;case"active-descendant-blur":r(!1)}};return e.addEventListener("active-descendant-focus",t),e.addEventListener("active-descendant-blur",t),()=>{e.removeEventListener("active-descendant-focus",t),e.removeEventListener("active-descendant-blur",t)}}),t),[a,n]}},50238:(e,t,n)=>{"use strict";n.d(t,{useRovingTabindexElement:()=>r});var o=n(50959),s=n(39416);function r(e,t=[]){const[n,r]=(0,o.useState)(!1),a=(0,s.useFunctionalRefObject)(e);return(0,o.useLayoutEffect)((()=>{const e=a.current;if(null===e)return;const t=e=>{switch(e.type){case"roving-tabindex:main-element":r(!0);break;case"roving-tabindex:secondary-element":r(!1)}};return e.addEventListener("roving-tabindex:main-element",t),e.addEventListener("roving-tabindex:secondary-element",t),()=>{e.removeEventListener("roving-tabindex:main-element",t),e.removeEventListener("roving-tabindex:secondary-element",t)}}),t),[a,n?0:-1]}},10772:(e,t,n)=>{"use strict";n.d(t,{ContextMenuAction:()=>x});var o=n(50959),s=n(50151),r=n(91561),a=n(59064),i=n(51768),l=n(63273);var c=n(83021),u=n(97754),d=n.n(u),h=n(26996),m=n(5304),p=n(22846);function v(e){const{size:t="normal"}=e;return o.createElement(m.ContextMenuItem,{size:t,jsxLabel:o.createElement("div",{className:d()(p.loaderWrap,p[t])},o.createElement(h.Loader,{className:p.loader})),noInteractive:!0,onMouseOver:e.onMouseOver})}var b=n(3343),f=n(50238),E=n(36110);const g=(0,o.forwardRef)((function(e,t){const{className:n,...s}=e,[r,a]=(0,f.useRovingTabindexElement)(t);return o.createElement(m.ContextMenuItem,{...s,className:d()(E.accessible,n),reference:r,tabIndex:a,onKeyDown:function(e){if(e.target!==e.currentTarget)return;const t=(0,b.hashFromEvent)(e) +;13!==t&&32!==t||(e.preventDefault(),r.current instanceof HTMLElement&&r.current.click())},"data-role":"menuitem","aria-disabled":e.disabled||void 0})}));var w=n(90186);class x extends o.PureComponent{constructor(e){super(e),this._itemRef=null,this._menuElementRef=o.createRef(),this._menuRef=null,this._handleClick=e=>{e.isDefaultPrevented()||this.state.disabled||(this._hasSubItems()?this._showSubMenu():(this.state.doNotCloseOnClick||(0,a.globalCloseMenu)(),this.props.action.execute(),this._trackEvent(),this.props.onExecute&&this.props.onExecute(this.props.action)))},this._handleClickToolbox=()=>{(0,a.globalCloseMenu)()},this._handleItemMouseOver=()=>{this._showSubMenu(),this._setCurrentContextValue()},this._handleMenuMouseOver=()=>{this._setCurrentContextValue()},this._showSubMenu=()=>{this.props.onShowSubMenu(this.props.action)},this._calcSubMenuPos=e=>function(e,t,n={x:0,y:10}){if(t){const{left:n,right:o,top:s}=t.getBoundingClientRect(),r=document.documentElement.clientWidth,a={x:n-e,y:s},i={x:o,y:s};return(0,l.isRtl)()?n<=e?i:a:r-o>=e?i:a}return n}(e.contentWidth,this._itemRef),this._updateState=e=>{this.setState(e.getState())},this._setItemRef=e=>{this._itemRef=e},this._handleMenuRef=e=>{this._menuRef=e},this._registerSubmenu=()=>this.context?.registerSubmenu(this.props.action.id,(e=>(0,s.ensureNotNull)(this._itemRef).contains(e)||null!==this._menuElementRef.current&&this._menuElementRef.current.contains(e))),this.state={...this.props.action.getState()}}componentDidMount(){this.props.action.onUpdate().subscribe(this,this._updateState),this.state.subItems.length&&(this._unsubscribe=this._registerSubmenu()),this.props.reference&&(this._itemRef=this.props.reference.current)}componentDidUpdate(e,t){t.loading!==this.state.loading&&this.props.onRequestUpdate?.(),0===t.subItems.length&&this.state.subItems.length>0&&(this._unsubscribe=this._registerSubmenu()),t.subItems.length>0&&0===this.state.subItems.length&&this._unsubscribe?.(),t.subItems!==this.state.subItems&&null!==this._menuRef&&this._menuRef.update()}componentWillUnmount(){this.props.action.onUpdate().unsubscribe(this,this._updateState),this._unsubscribe&&this._unsubscribe()}render(){const e=this.context?.current?this.context.current===this.props.action.id:this.props.isSubMenuOpened;return this.state.loading?o.createElement(v,{size:this.state.size}):o.createElement(g,{...(0,w.filterDataProps)(this.props),theme:this.props.theme,ref:this.props.reference??this._setItemRef,onClick:this._handleClick,onClickToolbox:this._handleClickToolbox,onMouseOver:this._handleItemMouseOver,hovered:e,hasSubItems:this._hasSubItems(),actionName:this.state.name,checkboxInput:this.props.checkboxInput,selected:this.props.selected,className:this.props.className,...this.state},o.createElement(r.ContextMenu,{isOpened:e,items:this.state.subItems,position:this._calcSubMenuPos,menuStatName:this.props.menuStatName,parentStatName:this._getStatName(),menuElementReference:this._menuElementRef,onMouseOver:this.state.subItems.length?this._handleMenuMouseOver:void 0,ref:this._handleMenuRef}))} +_setCurrentContextValue(){this.state.subItems.length&&this.context?.setCurrent(this.props.action.id)}_hasSubItems(){return this.state.subItems.length>0}_trackEvent(){const e=this._getStatName();(0,i.trackEvent)("ContextMenuClick",this.props.menuStatName||"",e)}_getStatName(){return[this.props.parentStatName,this.state.statName].filter((e=>Boolean(e))).join(".")}}x.contextType=c.SubmenuContext},5304:(e,t,n)=>{"use strict";n.d(t,{ContextMenuItem:()=>w,DEFAUL_CONTEXT_MENU_ITEM_THEME:()=>E});var o=n(50959),s=n(97754),r=n(70673),a=n(49483),i=n(71382),l=n(96040),c=n(36189),u=n(99025),d=n(25812),h=n(56570),m=n(90186),p=n(60925),v=n(60004),b=n(14665),f=n(31668);const E=f,g=h.enabled("items_favoriting");class w extends o.PureComponent{constructor(){super(...arguments),this._handleMouseOver=e=>{(0,i.isTouchEvent)(e.nativeEvent)||this.props.onMouseOver&&this.props.onMouseOver()},this._handleClickToolbox=e=>{e.stopPropagation(),this.props.onClickToolbox&&this.props.onClickToolbox()}}render(){const{hasSubItems:e,shortcutHint:t,hint:n,invisibleHotkey:r,favourite:i,theme:l=f,size:d="normal",onKeyDown:h,label:p,jsxLabel:v,styledLabel:E}=this.props,w=this.props.checkable&&this.props.checkboxInput?"label":"div";return o.createElement(o.Fragment,null,o.createElement("tr",{...(0,m.filterDataProps)(this.props),...(0,m.filterAriaProps)(this.props),id:this.props.id,className:s(this.props.className,l.item,!this.props.noInteractive&&l.interactive,this.props.hovered&&l.hovered,this.props.disabled&&l.disabled,this.props.active&&l.active,this.props.selected&&l.selected,l[d]),onClick:this.props.onClick,onMouseOver:this._handleMouseOver,ref:this.props.reference,"data-action-name":this.props.actionName,tabIndex:this.props.tabIndex,onKeyDown:h},g&&void 0!==i&&o.createElement("td",{className:l.favoriteActionCell},o.createElement(c.FavoriteButton,{id:`${this.props.id}-favorite`,className:l.favourite,isFilled:i,onClick:this.props.onFavouriteClick,"data-role":"list-item-action"})),o.createElement("td",{className:s(l.iconCell),"data-icon-cell":!0},this._icon(l)),o.createElement("td",{className:l.contentCell},o.createElement(w,{className:l.content},o.createElement("span",{className:s(l.label,this.props.checked&&l.checked),"data-label":!0},!v&&E?E.map((({text:e,...t},n)=>o.createElement("span",{key:n,style:t},e))):v??p),this._toolbox(l),e&&o.createElement("span",{className:l.arrowIcon,dangerouslySetInnerHTML:{__html:b},"data-submenu-arrow":!0}),!e&&t&&!a.CheckMobile.any()&&o.createElement(u.Hint,{className:s(r&&l.invisibleHotkey),text:t}),!e&&!t&&n&&o.createElement(u.Hint,{text:n})))),o.createElement("tr",{className:l.subMenu},o.createElement("td",null,this.props.children)))}_icon(e){if(this.props.checkable){if(this.props.checkboxInput)return o.createElement(r.CheckboxInput,{className:s(e.icon,e.checkboxInput),checked:this.props.checked});if(this.props.checked){const t=!this.props.icon&&!this.props.iconChecked,n=this.props.iconChecked||this.props.icon||v;return o.createElement("span",{className:e.icon,dangerouslySetInnerHTML:{__html:n}, +"data-icon-checkmark":t})}return this.props.icon?o.createElement("span",{className:e.icon,dangerouslySetInnerHTML:{__html:this.props.icon}}):o.createElement("span",{className:e.icon})}return this.props.icon?o.createElement("span",{className:e.icon,dangerouslySetInnerHTML:{__html:this.props.icon}}):null}_toolbox(e){return this.props.toolbox?o.createElement("span",{className:s(e.toolbox,this.props.showToolboxOnHover&&e.showToolboxOnHover),onClick:this._handleClickToolbox,"data-toolbox":!0},this._renderToolboxContent()):null}_renderToolboxContent(){return this.props.toolbox&&this.props.toolbox.type===d.ToolboxType.Delete?o.createElement(l.RemoveButton,{icon:p,onClick:this.props.toolbox.action}):null}}},91561:(e,t,n)=>{"use strict";n.d(t,{ContextMenu:()=>_,OverlapContextMenu:()=>M});var o=n(50959),s=n(97754),r=n.n(s),a=n(86431),i=n(27317),l=n(52778);class c extends o.PureComponent{constructor(){super(...arguments),this._handleKeyDown=e=>{e.keyCode===this.props.keyCode&&this.props.handler(e)}}componentDidMount(){document.addEventListener(this.props.eventType||"keydown",this._handleKeyDown,!1)}componentWillUnmount(){document.removeEventListener(this.props.eventType||"keydown",this._handleKeyDown,!1)}render(){return null}}var u=n(75535),d=n(37558),h=n(90692),m=n(20669);function p(e){return o.createElement("li",{className:m.separator})}var v=n(23829),b=n(41590),f=n(59064);function E(e){const t=e.action.custom()??e.action,[n,s]=(0,o.useState)((()=>t.getState())),[r,a]=(0,o.useState)(!1),i=!!n.subItems.length,l=i&&r;return(0,o.useEffect)((()=>{const e=()=>s(t.getState());return t.onUpdate().subscribe(null,e),()=>{t.onUpdate().unsubscribe(null,e)}}),[]),o.createElement(v.ContextMenuItem,{...n,onClick:function(e){if(n.disabled||e.defaultPrevented)return;if(i)return void a(!0);n.doNotCloseOnClick||(0,f.globalCloseMenu)();t.execute()},isLoading:n.loading,isHovered:l},l&&o.createElement(b.Drawer,{onClose:c},o.createElement(x,{items:n.subItems,parentAction:t,closeNested:c})));function c(e){e&&e.preventDefault(),a(!1)}}var g=n(54627),w=n(66493);function x(e){const{items:t,parentAction:n,closeNested:s}=e,r=!Boolean(n)&&t.every((e=>!Boolean("separator"!==e.type&&(e.getState().icon||e.getState().checkable))));return o.createElement(g.EmptyIconsContext.Provider,{value:r},o.createElement("ul",null,n&&o.createElement(o.Fragment,null,o.createElement(v.ContextMenuItem,{label:n.getState().label,isTitle:!0,active:!1,disabled:!1,subItems:[],checkable:!1,checked:!1,doNotCloseOnClick:!1,icon:w,onClick:s}),o.createElement(p,null)),t.map((e=>{switch(e.type){case"action":return o.createElement(E,{key:e.id,action:e});case"separator":return o.createElement(p,{key:e.id})}}))))}const k=o.createContext(null);var y=n(20243),C=n(37399);class _ extends o.PureComponent{constructor(e){super(e),this._menuRef=o.createRef(),this._handleRequestUpdate=()=>{this.update()},this._handleClose=()=>{this.props.onClose&&this.props.onClose()},this._handleOutsideClickClose=e=>{const{doNotCloseOn:t,onClose:n}=this.props;!n||void 0!==t&&t.contains(e.target)||n()}, +this._handleFocusOnOpen=()=>{this.props.menuElementReference?.current&&this.props.takeFocus&&this.props.menuElementReference?.current.focus({preventScroll:!0})},this._handleFocus=e=>{this.props.isKeyboardEvent&&e.target&&(0,y.focusFirstMenuItem)(e.target)},this.state={}}render(){const{isOpened:e,onClose:t,items:n,doNotCloseOn:s,menuStatName:a,parentStatName:m,takeFocus:p,...v}=this.props;return e&&n.length>0?o.createElement(d.DrawerManager,null,o.createElement(c,{keyCode:27,eventType:"keyup",handler:this._handleClose}),o.createElement(h.MatchMedia,{rule:"(max-width: 440px)"},(t=>this._isDrawer(t)?o.createElement(k.Provider,{value:{type:"drawer"}},o.createElement(b.Drawer,{onClose:this._handleClose,position:"Bottom","data-name":v["data-name"]},o.createElement(x,{items:n}))):o.createElement(k.Provider,{value:{type:"menu"}},o.createElement(l.OutsideEvent,{handler:this._handleOutsideClickClose,mouseDown:!0,touchStart:!0,reference:this.props.menuElementReference},(t=>o.createElement(i.Menu,{...v,reference:t,className:r()(C.menu,"context-menu"),onClose:this._handleClose,noMomentumBasedScroll:!0,ref:this._menuRef,tabIndex:p?-1:void 0,onOpen:this._handleFocusOnOpen,onFocus:this._handleFocus,onKeyDown:y.handleAccessibleMenuKeyDown},o.createElement(u.ActionsTable,{items:n,menuStatName:a,parentStatName:m,parentIsOpened:e,onRequestUpdate:this._handleRequestUpdate})))))))):null}update(){this._menuRef.current&&this._menuRef.current.update(),this.props.isKeyboardEvent&&this.props.menuElementReference?.current&&document.activeElement===this.props.menuElementReference.current&&(0,y.focusFirstMenuItem)(this.props.menuElementReference.current)}_isDrawer(e){return void 0===this.props.mode?e:"drawer"===this.props.mode}}const M=(0,a.makeOverlapable)(_)},99025:(e,t,n)=>{"use strict";n.d(t,{Hint:()=>i});var o=n(50959),s=n(97754),r=n.n(s),a=n(31668);function i(e){const{text:t="",className:n}=e;return o.createElement("span",{className:r()(a.shortcut,n)},t)}},23829:(e,t,n)=>{"use strict";n.d(t,{ContextMenuItem:()=>p});var o=n(50959),s=n(97754),r=n.n(s),a=n(9745),i=n(26996),l=n(54627),c=n(99025),u=n(39750),d=n(79978),h=n(60925),m=n(70159);function p(e){const{className:t,isTitle:n,isLoading:s,isHovered:p,active:v,checkable:b,disabled:f,checked:E,icon:g,iconChecked:w,hint:x,subItems:k,label:y,styledLabel:C,onClick:_,children:M,toolbox:S,jsxLabel:N,size:I="normal"}=e,D=(0,o.useContext)(l.EmptyIconsContext),L=!!k.length;return s?o.createElement("li",{className:r()(t,m.item,m.loading,m[I])},o.createElement(i.Loader,null)):o.createElement("li",{className:r()(t,m.item,m.interactive,n&&m.title,f&&m.disabled,p&&m.hovered,v&&m.active,D&&m.emptyIcons,m[I]),onClick:_},o.createElement(a.Icon,{className:r()(m.icon),icon:function(){if(b&&E)return w||g||u;return g}()}),o.createElement("span",{className:r()(m.label)},!N&&C?C.map((({text:e,...t},n)=>o.createElement("span",{key:n,style:t},e))):N??y),!!S&&o.createElement(a.Icon,{onClick:function(){S&&S.action()},className:m.remove,icon:h}),!L&&x&&o.createElement(c.Hint,{className:m.shortcut,text:x +}),L&&o.createElement(a.Icon,{className:m.nested,icon:d}),M)}},54627:(e,t,n)=>{"use strict";n.d(t,{EmptyIconsContext:()=>o});const o=n(50959).createContext(!1)},37558:(e,t,n)=>{"use strict";n.d(t,{DrawerContext:()=>a,DrawerManager:()=>r});var o=n(50959),s=n(99054);class r extends o.PureComponent{constructor(e){super(e),this._isBodyFixed=!1,this._addDrawer=e=>{this.setState((t=>({stack:[...t.stack,e]})))},this._removeDrawer=e=>{this.setState((t=>({stack:t.stack.filter((t=>t!==e))})))},this.state={stack:[]}}componentDidUpdate(e,t){!t.stack.length&&this.state.stack.length&&((0,s.setFixedBodyState)(!0),this._isBodyFixed=!0),t.stack.length&&!this.state.stack.length&&this._isBodyFixed&&((0,s.setFixedBodyState)(!1),this._isBodyFixed=!1)}componentWillUnmount(){this.state.stack.length&&this._isBodyFixed&&(0,s.setFixedBodyState)(!1)}render(){return o.createElement(a.Provider,{value:{addDrawer:this._addDrawer,removeDrawer:this._removeDrawer,currentDrawer:this.state.stack.length?this.state.stack[this.state.stack.length-1]:null}},this.props.children)}}const a=o.createContext(null)},41590:(e,t,n)=>{"use strict";n.d(t,{Drawer:()=>m});var o=n(50959),s=n(50151),r=n(97754),a=n(92184),i=n(42842),l=n(37558),c=n(29197),u=n(86656),d=n(36718);var h;function m(e){const{position:t="Bottom",onClose:n,children:u,reference:h,className:m,theme:v=d}=e,b=(0,s.ensureNotNull)((0,o.useContext)(l.DrawerContext)),[f]=(0,o.useState)((()=>(0,a.randomHash)())),E=(0,o.useRef)(null),g=(0,o.useContext)(c.CloseDelegateContext);return(0,o.useLayoutEffect)((()=>((0,s.ensureNotNull)(E.current).focus({preventScroll:!0}),g.subscribe(b,n),b.addDrawer(f),()=>{b.removeDrawer(f),g.unsubscribe(b,n)})),[]),o.createElement(i.Portal,null,o.createElement("div",{ref:h,className:r(d.wrap,d[`position${t}`])},f===b.currentDrawer&&o.createElement("div",{className:d.backdrop,onClick:n}),o.createElement(p,{className:r(v.drawer,d[`position${t}`],m),ref:E,"data-name":e["data-name"]},u)))}!function(e){e.Left="Left",e.Bottom="Bottom"}(h||(h={}));const p=(0,o.forwardRef)(((e,t)=>{const{className:n,...s}=e;return o.createElement(u.TouchScrollContainer,{className:r(d.drawer,n),tabIndex:-1,ref:t,...s})}))},71402:(e,t,n)=>{"use strict";n.d(t,{RemoveTitleType:()=>o,removeTitlesMap:()=>r});var o,s=n(11542);!function(e){e.Add="add",e.Remove="remove"}(o||(o={}));const r={[o.Add]:s.t(null,void 0,n(69207)),[o.Remove]:s.t(null,void 0,n(85106))}},36189:(e,t,n)=>{"use strict";n.d(t,{FavoriteButton:()=>h});var o=n(50959),s=n(97754),r=n.n(s),a=n(9745),i=n(71402),l=n(74670),c=n(39146),u=n(48010),d=n(22413);function h(e){const{className:t,isFilled:n,isActive:s,onClick:h,title:m,...p}=e,[v,b]=(0,l.useActiveDescendant)(null),f=m??(n?i.removeTitlesMap[i.RemoveTitleType.Remove]:i.removeTitlesMap[i.RemoveTitleType.Add]);return(0,o.useLayoutEffect)((()=>{const e=v.current;e instanceof HTMLElement&&f&&e.dispatchEvent(new CustomEvent("common-tooltip-update"))}),[f,v]),o.createElement(a.Icon,{...p,className:r()(d.favorite,"apply-common-tooltip",n&&d.checked,s&&d.active,b&&d.focused,t),onClick:h,icon:n?c:u, +title:f,ariaLabel:f,ref:v})}},96040:(e,t,n)=>{"use strict";n.d(t,{RemoveButton:()=>d});var o=n(11542),s=n(50959),r=n(97754),a=n.n(r),i=n(9745),l=n(74670),c=n(33765),u=n(35990);function d(e){const{className:t,isActive:r,onClick:d,onMouseDown:h,title:m,hidden:p,"data-name":v="remove-button",icon:b,...f}=e,[E,g]=(0,l.useActiveDescendant)(null);return s.createElement(i.Icon,{...f,"data-name":v,className:a()(u.button,"apply-common-tooltip",r&&u.active,p&&u.hidden,g&&u.focused,t),icon:b||c,onClick:d,onMouseDown:h,title:m??o.t(null,void 0,n(67410)),ariaLabel:m??o.t(null,void 0,n(67410)),ref:E})}},86656:(e,t,n)=>{"use strict";n.d(t,{TouchScrollContainer:()=>c});var o=n(50959),s=n(59142),r=n(50151),a=n(49483);const i=CSS.supports("overscroll-behavior","none");let l=0;const c=(0,o.forwardRef)(((e,t)=>{const{children:n,...r}=e,c=(0,o.useRef)(null);return(0,o.useImperativeHandle)(t,(()=>c.current)),(0,o.useLayoutEffect)((()=>{if(a.CheckMobile.iOS())return l++,null!==c.current&&(i?1===l&&(document.body.style.overscrollBehavior="none"):(0,s.disableBodyScroll)(c.current,{allowTouchMove:u(c)})),()=>{l--,null!==c.current&&(i?0===l&&(document.body.style.overscrollBehavior=""):(0,s.enableBodyScroll)(c.current))}}),[]),o.createElement("div",{ref:c,...r},n)}));function u(e){return t=>{const n=(0,r.ensureNotNull)(e.current),o=document.activeElement;return!n.contains(t)||null!==o&&n.contains(o)&&o.contains(t)}}},20243:(e,t,n)=>{"use strict";n.d(t,{focusFirstMenuItem:()=>u,handleAccessibleMenuFocus:()=>l,handleAccessibleMenuKeyDown:()=>c,queryMenuElements:()=>m});var o=n(19291),s=n(57177),r=n(68335),a=n(15754);const i=[37,39,38,40];function l(e,t){if(!e.target)return;const n=e.relatedTarget?.getAttribute("aria-activedescendant");if(e.relatedTarget!==t.current){const e=n&&document.getElementById(n);if(!e||e!==t.current)return}u(e.target)}function c(e){if(e.defaultPrevented)return;const t=(0,r.hashFromEvent)(e);if(!i.includes(t))return;const n=document.activeElement;if(!(document.activeElement instanceof HTMLElement))return;const a=m(e.currentTarget).sort(o.navigationOrderComparator);if(0===a.length)return;const l=document.activeElement.closest('[data-role="menuitem"]')||document.activeElement.parentElement?.querySelector('[data-role="menuitem"]');if(!(l instanceof HTMLElement))return;const c=a.indexOf(l);if(-1===c)return;const u=p(l),v=u.indexOf(document.activeElement),b=-1!==v,f=e=>{n&&(0,s.becomeSecondaryElement)(n),(0,s.becomeMainElement)(e),e.focus()};switch((0,o.mapKeyCodeToDirection)(t)){case"inlinePrev":if(!u.length)return;e.preventDefault(),f(0===v?a[c]:b?d(u,v,-1):u[u.length-1]);break;case"inlineNext":if(!u.length)return;e.preventDefault(),v===u.length-1?f(a[c]):f(b?d(u,v,1):u[0]);break;case"blockPrev":{e.preventDefault();const t=d(a,c,-1);if(b){const e=h(t,v);f(e||t);break}f(t);break}case"blockNext":{e.preventDefault();const t=d(a,c,1);if(b){const e=h(t,v);f(e||t);break}f(t)}}}function u(e){const[t]=m(e);t&&((0,s.becomeMainElement)(t),t.focus())}function d(e,t,n){return e[(t+e.length+n)%e.length]}function h(e,t){const n=p(e) +;return n.length?n[(t+n.length)%n.length]:null}function m(e){return Array.from(e.querySelectorAll('[data-role="menuitem"]:not([disabled]):not([aria-disabled])')).filter((0,a.createScopedVisibleElementFilter)(e))}function p(e){return Array.from(e.querySelectorAll("[tabindex]:not([disabled]):not([aria-disabled])")).filter((0,a.createScopedVisibleElementFilter)(e))}},57177:(e,t,n)=>{"use strict";var o;function s(e){e.dispatchEvent(new CustomEvent("roving-tabindex:main-element"))}function r(e){e.dispatchEvent(new CustomEvent("roving-tabindex:secondary-element"))}n.d(t,{becomeMainElement:()=>s,becomeSecondaryElement:()=>r}),function(e){e.MainElement="roving-tabindex:main-element",e.SecondaryElement="roving-tabindex:secondary-element"}(o||(o={}))},75535:(e,t,n)=>{"use strict";n.d(t,{ActionsTable:()=>i});var o=n(50959),s=n(21320);function r(e){return o.createElement("tr",{className:s.row},o.createElement("td",null,o.createElement("div",{className:s.line})),o.createElement("td",null,o.createElement("div",{className:s.line}),e.hint?o.createElement("div",{className:s.hint},e.hint):null))}var a=n(10772);class i extends o.PureComponent{constructor(e){super(e),this._handleShowSubMenu=e=>{const t=e.getState();this.setState({showSubMenuOf:t.subItems.length?e:void 0})},this.state={}}render(){return o.createElement("table",null,o.createElement("tbody",null,this.props.items.map((e=>this._item(e)))))}static getDerivedStateFromProps(e,t){return!e.parentIsOpened&&t.showSubMenuOf?{showSubMenuOf:void 0}:null}_item(e){switch(e.type){case"separator":return o.createElement(r,{key:e.id,hint:e.getHint()});case"action":const t=e.custom()??e;return o.createElement(a.ContextMenuAction,{key:t.id,action:t,onShowSubMenu:this._handleShowSubMenu,isSubMenuOpened:this.state.showSubMenuOf===t,menuStatName:this.props.menuStatName,parentStatName:this.props.parentStatName,onRequestUpdate:this.props.onRequestUpdate})}}}},60925:e=>{e.exports=''},60004:e=>{e.exports=''},65890:e=>{e.exports=''},66493:e=>{e.exports=''},79978:e=>{ +e.exports=''},39750:e=>{e.exports=''},33765:e=>{e.exports=''},14665:e=>{e.exports=''},39146:e=>{e.exports=''},48010:e=>{e.exports=''}}]); \ No newline at end of file diff --git a/frontend/charting_library/bundles/2197.3c275591170ccafa3bbe.css b/frontend/charting_library/bundles/2197.3c275591170ccafa3bbe.css new file mode 100644 index 0000000..e8b1db8 --- /dev/null +++ b/frontend/charting_library/bundles/2197.3c275591170ccafa3bbe.css @@ -0,0 +1 @@ +.title-u3QJgF_p{cursor:default;font-size:11px;letter-spacing:.4px;line-height:16px;margin:6px 0;padding:0 12px;text-transform:uppercase}.title-u3QJgF_p,html.theme-dark .title-u3QJgF_p{color:var(--tv-color-popup-element-secondary-text,var(--themed-color-popup-element-secondary-text,grey))} \ No newline at end of file diff --git a/frontend/charting_library/bundles/2197.3c275591170ccafa3bbe.rtl.css b/frontend/charting_library/bundles/2197.3c275591170ccafa3bbe.rtl.css new file mode 100644 index 0000000..e8b1db8 --- /dev/null +++ b/frontend/charting_library/bundles/2197.3c275591170ccafa3bbe.rtl.css @@ -0,0 +1 @@ +.title-u3QJgF_p{cursor:default;font-size:11px;letter-spacing:.4px;line-height:16px;margin:6px 0;padding:0 12px;text-transform:uppercase}.title-u3QJgF_p,html.theme-dark .title-u3QJgF_p{color:var(--tv-color-popup-element-secondary-text,var(--themed-color-popup-element-secondary-text,grey))} \ No newline at end of file diff --git a/frontend/charting_library/bundles/2227.c1c4b4d4d12f9774793f.js b/frontend/charting_library/bundles/2227.c1c4b4d4d12f9774793f.js new file mode 100644 index 0000000..e8ab28f --- /dev/null +++ b/frontend/charting_library/bundles/2227.c1c4b4d4d12f9774793f.js @@ -0,0 +1,46 @@ +"use strict";(self.webpackChunktradingview=self.webpackChunktradingview||[]).push([[2227],{82321:(e,n,t)=>{var r=t(50959),l=t(22962);function a(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,t=1;t