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. + + + + + +## ✨ 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** 的数据。请参阅下方[部署](#-部署)章节了解如何配置。 + + + + + +## ✨ 功能 + +### 数据采集与存储 +- [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;tn}return!1}(n,t,l,r)&&(t=null),r||null===l?function(e){return!!f.call(m,e)||!f.call(p,e)&&(d.test(e)?m[e]=!0:(p[e]=!0,!1))}(n)&&(null===t?e.removeAttribute(n):e.setAttribute(n,""+t)):l.mustUseProperty?e[l.propertyName]=null===t?3!==l.type&&"":t:(n=l.attributeName,r=l.attributeNamespace,null===t?e.removeAttribute(n):(t=3===(l=l.type)||4===l&&!0===t?"":""+t,r?e.setAttributeNS(r,n,t):e.setAttribute(n,t))))}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach((function(e){var n=e.replace(v,y);g[n]=new h(n,1,!1,e,null,!1,!1)})),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach((function(e){var n=e.replace(v,y);g[n]=new h(n,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)})),["xml:base","xml:lang","xml:space"].forEach((function(e){var n=e.replace(v,y);g[n]=new h(n,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)})),["tabIndex","crossOrigin"].forEach((function(e){g[e]=new h(e,1,!1,e.toLowerCase(),null,!1,!1)})),g.xlinkHref=new h("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach((function(e){g[e]=new h(e,1,!1,e.toLowerCase(),null,!0,!0)}));var k=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,w=Symbol.for("react.element"),S=Symbol.for("react.portal"),x=Symbol.for("react.fragment"),E=Symbol.for("react.strict_mode"),C=Symbol.for("react.profiler"),_=Symbol.for("react.provider"),P=Symbol.for("react.context"),N=Symbol.for("react.forward_ref"),z=Symbol.for("react.suspense"),T=Symbol.for("react.suspense_list"),L=Symbol.for("react.memo"),R=Symbol.for("react.lazy");Symbol.for("react.scope"),Symbol.for("react.debug_trace_mode");var M=Symbol.for("react.offscreen");Symbol.for("react.legacy_hidden"),Symbol.for("react.cache"),Symbol.for("react.tracing_marker");var F=Symbol.iterator;function O(e){ +return null===e||"object"!=typeof e?null:"function"==typeof(e=F&&e[F]||e["@@iterator"])?e:null}var D,I=Object.assign;function U(e){if(void 0===D)try{throw Error()}catch(e){var n=e.stack.trim().match(/\n( *(at )?)/);D=n&&n[1]||""}return"\n"+D+e}var V=!1;function A(e,n){if(!e||V)return"";V=!0;var t=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(n)if(n=function(){throw Error()},Object.defineProperty(n.prototype,"props",{set:function(){throw Error()}}),"object"==typeof Reflect&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}e.call(n.prototype)}else{try{throw Error()}catch(e){r=e}e()}}catch(n){if(n&&r&&"string"==typeof n.stack){for(var l=n.stack.split("\n"),a=r.stack.split("\n"),u=l.length-1,o=a.length-1;1<=u&&0<=o&&l[u]!==a[o];)o--;for(;1<=u&&0<=o;u--,o--)if(l[u]!==a[o]){if(1!==u||1!==o)do{if(u--,0>--o||l[u]!==a[o]){var i="\n"+l[u].replace(" at new "," at ");return e.displayName&&i.includes("")&&(i=i.replace("",e.displayName)),i}}while(1<=u&&0<=o);break}}}finally{V=!1,Error.prepareStackTrace=t}return(e=e?e.displayName||e.name:"")?U(e):""}function $(e){switch(e.tag){case 5:return U(e.type);case 16:return U("Lazy");case 13:return U("Suspense");case 19:return U("SuspenseList");case 0:case 2:case 15:return e=A(e.type,!1);case 11:return e=A(e.type.render,!1);case 1:return e=A(e.type,!0);default:return""}}function j(e){if(null==e)return null;if("function"==typeof e)return e.displayName||e.name||null;if("string"==typeof e)return e;switch(e){case x:return"Fragment";case S:return"Portal";case C:return"Profiler";case E:return"StrictMode";case z:return"Suspense";case T:return"SuspenseList"}if("object"==typeof e)switch(e.$$typeof){case P:return(e.displayName||"Context")+".Consumer";case _:return(e._context.displayName||"Context")+".Provider";case N:var n=e.render;return(e=e.displayName)||(e=""!==(e=n.displayName||n.name||"")?"ForwardRef("+e+")":"ForwardRef"),e;case L:return null!==(n=e.displayName||null)?n:j(e.type)||"Memo";case R:n=e._payload,e=e._init;try{return j(e(n))}catch(e){}}return null}function B(e){var n=e.type;switch(e.tag){case 24:return"Cache";case 9:return(n.displayName||"Context")+".Consumer";case 10:return(n._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=(e=n.render).displayName||e.name||"",n.displayName||(""!==e?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return n;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return j(n);case 8:return n===E?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"==typeof n)return n.displayName||n.name||null;if("string"==typeof n)return n}return null}function H(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":case"object":return e;default:return""}}function W(e){var n=e.type +;return(e=e.nodeName)&&"input"===e.toLowerCase()&&("checkbox"===n||"radio"===n)}function Q(e){e._valueTracker||(e._valueTracker=function(e){var n=W(e)?"checked":"value",t=Object.getOwnPropertyDescriptor(e.constructor.prototype,n),r=""+e[n];if(!e.hasOwnProperty(n)&&void 0!==t&&"function"==typeof t.get&&"function"==typeof t.set){var l=t.get,a=t.set;return Object.defineProperty(e,n,{configurable:!0,get:function(){return l.call(this)},set:function(e){r=""+e,a.call(this,e)}}),Object.defineProperty(e,n,{enumerable:t.enumerable}),{getValue:function(){return r},setValue:function(e){r=""+e},stopTracking:function(){e._valueTracker=null,delete e[n]}}}}(e))}function q(e){if(!e)return!1;var n=e._valueTracker;if(!n)return!0;var t=n.getValue(),r="";return e&&(r=W(e)?e.checked?"true":"false":e.value),(e=r)!==t&&(n.setValue(e),!0)}function K(e){if(void 0===(e=e||("undefined"!=typeof document?document:void 0)))return null;try{return e.activeElement||e.body}catch(n){return e.body}}function Y(e,n){var t=n.checked;return I({},n,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=t?t:e._wrapperState.initialChecked})}function X(e,n){var t=null==n.defaultValue?"":n.defaultValue,r=null!=n.checked?n.checked:n.defaultChecked;t=H(null!=n.value?n.value:t),e._wrapperState={initialChecked:r,initialValue:t,controlled:"checkbox"===n.type||"radio"===n.type?null!=n.checked:null!=n.value}}function G(e,n){null!=(n=n.checked)&&b(e,"checked",n,!1)}function Z(e,n){G(e,n);var t=H(n.value),r=n.type;if(null!=t)"number"===r?(0===t&&""===e.value||e.value!=t)&&(e.value=""+t):e.value!==""+t&&(e.value=""+t);else if("submit"===r||"reset"===r)return void e.removeAttribute("value");n.hasOwnProperty("value")?ee(e,n.type,t):n.hasOwnProperty("defaultValue")&&ee(e,n.type,H(n.defaultValue)),null==n.checked&&null!=n.defaultChecked&&(e.defaultChecked=!!n.defaultChecked)}function J(e,n,t){if(n.hasOwnProperty("value")||n.hasOwnProperty("defaultValue")){var r=n.type;if(!("submit"!==r&&"reset"!==r||void 0!==n.value&&null!==n.value))return;n=""+e._wrapperState.initialValue,t||n===e.value||(e.value=n),e.defaultValue=n}""!==(t=e.name)&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,""!==t&&(e.name=t)}function ee(e,n,t){"number"===n&&K(e.ownerDocument)===e||(null==t?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+t&&(e.defaultValue=""+t))}var ne=Array.isArray;function te(e,n,t,r){if(e=e.options,n){n={};for(var l=0;l"+n.valueOf().toString()+"",n=se.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;n.firstChild;)e.appendChild(n.firstChild)}},"undefined"!=typeof MSApp&&MSApp.execUnsafeLocalFunction?function(e,n,t,r){MSApp.execUnsafeLocalFunction((function(){return ce(e,n)}))}:ce);function de(e,n){if(n){var t=e.firstChild;if(t&&t===e.lastChild&&3===t.nodeType)return void(t.nodeValue=n)}e.textContent=n}var pe={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},me=["Webkit","ms","Moz","O"];function he(e,n,t){return null==n||"boolean"==typeof n||""===n?"":t||"number"!=typeof n||0===n||pe.hasOwnProperty(e)&&pe[e]?(""+n).trim():n+"px"}function ge(e,n){for(var t in e=e.style,n)if(n.hasOwnProperty(t)){var r=0===t.indexOf("--"),l=he(t,n[t],r);"float"===t&&(t="cssFloat"),r?e.setProperty(t,l):e[t]=l}}Object.keys(pe).forEach((function(e){me.forEach((function(n){n=n+e.charAt(0).toUpperCase()+e.substring(1),pe[n]=pe[e]}))}));var ve=I({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ye(e,n){if(n){if(ve[e]&&(null!=n.children||null!=n.dangerouslySetInnerHTML))throw Error(a(137,e));if(null!=n.dangerouslySetInnerHTML){if(null!=n.children)throw Error(a(60));if("object"!=typeof n.dangerouslySetInnerHTML||!("__html"in n.dangerouslySetInnerHTML))throw Error(a(61))}if(null!=n.style&&"object"!=typeof n.style)throw Error(a(62))}}function be(e,n){if(-1===e.indexOf("-"))return"string"==typeof n.is;switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src": +case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ke=null;function we(e){return(e=e.target||e.srcElement||window).correspondingUseElement&&(e=e.correspondingUseElement),3===e.nodeType?e.parentNode:e}var Se=null,xe=null,Ee=null;function Ce(e){if(e=bl(e)){if("function"!=typeof Se)throw Error(a(280));var n=e.stateNode;n&&(n=wl(n),Se(e.stateNode,e.type,n))}}function _e(e){xe?Ee?Ee.push(e):Ee=[e]:xe=e}function Pe(){if(xe){var e=xe,n=Ee;if(Ee=xe=null,Ce(e),n)for(e=0;e>>=0,0===e?32:31-(on(e)/sn|0)|0},on=Math.log,sn=Math.LN2;var cn=64,fn=4194304;function dn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return 4194240&e;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return 130023424&e;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function pn(e,n){var t=e.pendingLanes;if(0===t)return 0;var r=0,l=e.suspendedLanes,a=e.pingedLanes,u=268435455&t;if(0!==u){var o=u&~l;0!==o?r=dn(o):0!==(a&=u)&&(r=dn(a))}else 0!==(u=t&~l)?r=dn(u):0!==a&&(r=dn(a));if(0===r)return 0;if(0!==n&&n!==r&&!(n&l)&&((l=r&-r)>=(a=n&-n)||16===l&&4194240&a))return n;if(4&r&&(r|=16&t),0!==(n=e.entangledLanes))for(e=e.entanglements,n&=r;0t;t++)n.push(e);return n}function yn(e,n,t){e.pendingLanes|=n,536870912!==n&&(e.suspendedLanes=0,e.pingedLanes=0),(e=e.eventTimes)[n=31-un(n)]=t}function bn(e,n){var t=e.entangledLanes|=n;for(e=e.entanglements;t;){var r=31-un(t),l=1<=Ot),Ut=String.fromCharCode(32),Vt=!1;function At(e,n){switch(e){case"keyup":return-1!==Mt.indexOf(n.keyCode);case"keydown":return 229!==n.keyCode;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function $t(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var jt=!1;var Bt={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function Ht(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return"input"===n?!!Bt[e.type]:"textarea"===n}function Wt(e,n,t,r){_e(r),0<(n=Qr(n,"onChange")).length&&(t=new ct("onChange","change",null,t,r),e.push({event:t,listeners:n}))}var Qt=null,qt=null;function Kt(e){Ur(e,0)}function Yt(e){if(q(kl(e)))return e}function Xt(e,n){if("change"===e)return n}var Gt=!1;if(c){var Zt;if(c){var Jt="oninput"in document;if(!Jt){var er=document.createElement("div");er.setAttribute("oninput","return;"),Jt="function"==typeof er.oninput}Zt=Jt +}else Zt=!1;Gt=Zt&&(!document.documentMode||9=n)return{node:r,offset:n-e};e=t}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=sr(r)}}function fr(e,n){return!(!e||!n)&&(e===n||(!e||3!==e.nodeType)&&(n&&3===n.nodeType?fr(e,n.parentNode):"contains"in e?e.contains(n):!!e.compareDocumentPosition&&!!(16&e.compareDocumentPosition(n))))}function dr(){for(var e=window,n=K();n instanceof e.HTMLIFrameElement;){try{var t="string"==typeof n.contentWindow.location.href}catch(e){t=!1}if(!t)break;n=K((e=n.contentWindow).document)}return n}function pr(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return n&&("input"===n&&("text"===e.type||"search"===e.type||"tel"===e.type||"url"===e.type||"password"===e.type)||"textarea"===n||"true"===e.contentEditable)}function mr(e){var n=dr(),t=e.focusedElem,r=e.selectionRange;if(n!==t&&t&&t.ownerDocument&&fr(t.ownerDocument.documentElement,t)){if(null!==r&&pr(t))if(n=r.start,void 0===(e=r.end)&&(e=n),"selectionStart"in t)t.selectionStart=n,t.selectionEnd=Math.min(e,t.value.length);else if((e=(n=t.ownerDocument||document)&&n.defaultView||window).getSelection){e=e.getSelection();var l=t.textContent.length,a=Math.min(r.start,l);r=void 0===r.end?a:Math.min(r.end,l),!e.extend&&a>r&&(l=r,r=a,a=l),l=cr(t,a);var u=cr(t,r);l&&u&&(1!==e.rangeCount||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==u.node||e.focusOffset!==u.offset)&&((n=n.createRange()).setStart(l.node,l.offset),e.removeAllRanges(),a>r?(e.addRange(n),e.extend(u.node,u.offset)):(n.setEnd(u.node,u.offset),e.addRange(n)))}for(n=[],e=t;e=e.parentNode;)1===e.nodeType&&n.push({element:e,left:e.scrollLeft,top:e.scrollTop});for("function"==typeof t.focus&&t.focus(),t=0;t=document.documentMode,gr=null,vr=null,yr=null,br=!1;function kr(e,n,t){var r=t.window===t?t.document:9===t.nodeType?t:t.ownerDocument;br||null==gr||gr!==K(r)||("selectionStart"in(r=gr)&&pr(r)?r={start:r.selectionStart,end:r.selectionEnd +}:r={anchorNode:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection()).anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset},yr&&ir(yr,r)||(yr=r,0<(r=Qr(vr,"onSelect")).length&&(n=new ct("onSelect","select",null,n,t),e.push({event:n,listeners:r}),n.target=gr)))}function wr(e,n){var t={};return t[e.toLowerCase()]=n.toLowerCase(),t["Webkit"+e]="webkit"+n,t["Moz"+e]="moz"+n,t}var Sr={animationend:wr("Animation","AnimationEnd"),animationiteration:wr("Animation","AnimationIteration"),animationstart:wr("Animation","AnimationStart"),transitionend:wr("Transition","TransitionEnd")},xr={},Er={};function Cr(e){if(xr[e])return xr[e];if(!Sr[e])return e;var n,t=Sr[e];for(n in t)if(t.hasOwnProperty(n)&&n in Er)return xr[e]=t[n];return e}c&&(Er=document.createElement("div").style,"AnimationEvent"in window||(delete Sr.animationend.animation,delete Sr.animationiteration.animation,delete Sr.animationstart.animation),"TransitionEvent"in window||delete Sr.transitionend.transition);var _r=Cr("animationend"),Pr=Cr("animationiteration"),Nr=Cr("animationstart"),zr=Cr("transitionend"),Tr=new Map,Lr="abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");function Rr(e,n){Tr.set(e,n),i(n,[e])}for(var Mr=0;Mrxl||(e.current=Sl[xl],Sl[xl]=null,xl--)}function _l(e,n){xl++,Sl[xl]=e.current,e.current=n} +var Pl={},Nl=El(Pl),zl=El(!1),Tl=Pl;function Ll(e,n){var t=e.type.contextTypes;if(!t)return Pl;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===n)return r.__reactInternalMemoizedMaskedChildContext;var l,a={};for(l in t)a[l]=n[l];return r&&((e=e.stateNode).__reactInternalMemoizedUnmaskedChildContext=n,e.__reactInternalMemoizedMaskedChildContext=a),a}function Rl(e){return null!=(e=e.childContextTypes)}function Ml(){Cl(zl),Cl(Nl)}function Fl(e,n,t){if(Nl.current!==Pl)throw Error(a(168));_l(Nl,n),_l(zl,t)}function Ol(e,n,t){var r=e.stateNode;if(n=n.childContextTypes,"function"!=typeof r.getChildContext)return t;for(var l in r=r.getChildContext())if(!(l in n))throw Error(a(108,B(e)||"Unknown",l));return I({},t,r)}function Dl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Pl,Tl=Nl.current,_l(Nl,e),_l(zl,zl.current),!0}function Il(e,n,t){var r=e.stateNode;if(!r)throw Error(a(169));t?(e=Ol(e,n,Tl),r.__reactInternalMemoizedMergedChildContext=e,Cl(zl),Cl(Nl),_l(Nl,e)):Cl(zl),_l(zl,t)}var Ul=null,Vl=!1,Al=!1;function $l(e){null===Ul?Ul=[e]:Ul.push(e)}function jl(){if(!Al&&null!==Ul){Al=!0;var e=0,n=kn;try{var t=Ul;for(kn=1;e>=u,l-=u,Xl=1<<32-un(n)+l|t<h?(g=f,f=null):g=f.sibling;var v=p(l,f,o[h],i);if(null===v){null===f&&(f=g);break}e&&f&&null===v.alternate&&n(l,f),a=u(v,a,h),null===c?s=v:c.sibling=v,c=v,f=g}if(h===o.length)return t(l,f),la&&Zl(l,h),s;if(null===f){for(;h
Sign in with your authorized Google account to continue