diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..9a6377c --- /dev/null +++ b/web/.env.example @@ -0,0 +1 @@ +IPADS_API_KEY= diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..f238711 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +.env +.DS_Store +*.log +.qoder/ diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..5ebc818 --- /dev/null +++ b/web/README.md @@ -0,0 +1,70 @@ +# Personal Stock Agent + +统一个人股票监控 MVP,支持 A股、港股、美股自选股,按日生成监控报告。数据接口固定使用 IPADS Responses API,密钥从 `.env` 的 `IPADS_API_KEY` 读取。 + +## Run + +```bash +npm install +npm run dev +``` + +Frontend: http://127.0.0.1:5173 + +Backend: http://127.0.0.1:8787 + +LAN access: + +```bash +HOST=0.0.0.0 CLIENT_PORT=5174 PORT=8787 npm run dev +``` + +Open `http://gahow-pc.ipads-lab.se.sjtu.edu.cn:5174` from another device on the same network. + +## IPADS API + +配置文件: + +```env +IPADS_API_KEY=your_api_key +``` + +固定服务端配置: + +```text +base_url = "http://tianx.ipads-lab.se.sjtu.edu.cn:8319/v1" +wire_api = "responses" +env_key = "IPADS_API_KEY" +``` + +The backend calls: + +```text +POST http://tianx.ipads-lab.se.sjtu.edu.cn:8319/v1/responses +Authorization: Bearer ${IPADS_API_KEY} +``` + +The response output text must be JSON matching the stock snapshot schema used by the agent. If `IPADS_API_KEY` is blank, the app uses deterministic demo data so the dashboard remains usable. + +## Features + +- Add/remove watchlist symbols such as `600519`, `000001.SZ`, `00700.HK`, `AAPL`, `MSFT.US`. +- Normalize symbols into A股、港股、美股 markets. +- Generate a daily report with earnings timing, peer stocks, valuation discount/premium, related news, and buy-watch/hold/avoid recommendations. +- Refreshes the cached daily report every 24 hours while the backend process is running. +- View each watchlist stock with locally cached D/W/M/Y K-line chart data, zoom controls, MA5/MA20/MA60, RSI14, MACD, volume, and cached financial statements. US K-line and quote/research fields use Nasdaq as the fixed data source and target at least 5 years of verified daily OHLCV history when available. +- Cache related market context such as sector, liquidity, volatility, drawdown, relative strength, capital flow, sentiment, and risk flags. +- Run an agent-backed quantitative analysis across technical, valuation, growth, quality, liquidity, sentiment, and risk factors to classify each stock as buy-watch/hold/avoid. +- Add tags to every watchlist stock and filter the watchlist by tag. +- In configured provider mode, prompts require verified data and do not permit approximate or fabricated values; missing data is surfaced instead of being filled with synthetic values. +- K-line cache updates incrementally: once 5 years of daily bars exist locally, refreshes request only the latest missing window and merge by date. + +## Local cache + +Persistent app data defaults to `./data` and can be moved with: + +```env +STOCK_AGENT_DATA_DIR=/path/to/stock-agent-data +``` + +The backend stores watchlist data in `watchlist.json` and per-stock cached snapshot/K-line/financial data under `cache/`. diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7c00985 --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + Personal Stock Monitor + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..65b6e16 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,3491 @@ +{ + "name": "personal-stock-agent", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "personal-stock-agent", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "cors": "^2.8.5", + "dotenv": "^17.4.2", + "express": "^5.1.0", + "lightweight-charts": "^5.2.0", + "lucide-react": "^1.14.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/node": "^24.3.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "concurrently": "^9.2.1", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "vite": "^7.1.3", + "vitest": "^3.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.3.tgz", + "integrity": "sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fancy-canvas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightweight-charts": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-5.2.0.tgz", + "integrity": "sha512-ey3Vas8UhV06ni+LT9TA1nEe4y8So4Mi6CL/oarNHFMyTktz/xy8e8+oh04Q//eO3t6etvFXgayz2fClyFQb5w==", + "license": "Apache-2.0", + "dependencies": { + "fancy-canvas": "2.1.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..d055849 --- /dev/null +++ b/web/package.json @@ -0,0 +1,36 @@ +{ + "name": "personal-stock-agent", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently \"npm:dev:server\" \"npm:dev:client\"", + "dev:server": "tsx watch src/server/index.ts", + "dev:client": "vite", + "test": "vitest run", + "build": "tsc --noEmit && vite build" + }, + "dependencies": { + "@vitejs/plugin-react": "^5.0.0", + "cors": "^2.8.5", + "dotenv": "^17.4.2", + "express": "^5.1.0", + "lightweight-charts": "^5.2.0", + "lucide-react": "^1.14.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "zod": "^4.1.5" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/node": "^24.3.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "concurrently": "^9.2.1", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "vite": "^7.1.3", + "vitest": "^3.2.4" + } +} diff --git a/web/scripts/start-server.sh b/web/scripts/start-server.sh new file mode 100755 index 0000000..77687f8 --- /dev/null +++ b/web/scripts/start-server.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT_DIR}" + +export PATH="/home/gahow/.local/bin:/home/gahow/.nvm/versions/node/v24.13.1/bin:$PATH" +export HOST="127.0.0.1" +export PORT="${PORT:-8787}" +# Node's built-in fetch does not read HTTPS_PROXY unless this opt-in is enabled. +# The configured Nasdaq/Yahoo data sources require the host's local proxy. +export NODE_USE_ENV_PROXY="${NODE_USE_ENV_PROXY:-1}" + +exec npx tsx src/server/index.ts diff --git a/web/scripts/start-vite.sh b/web/scripts/start-vite.sh new file mode 100755 index 0000000..8e72090 --- /dev/null +++ b/web/scripts/start-vite.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT_DIR}" + +export PATH="/home/gahow/.nvm/versions/node/v24.13.1/bin:$PATH" + +# The public HTTPS entrypoint is nginx on :5443. Keep Vite as a localhost-only +# upstream so the development server is not exposed as a second LAN entrypoint. +exec npx vite --host 127.0.0.1 --port 5174 diff --git a/web/src/agent/indicators.ts b/web/src/agent/indicators.ts new file mode 100644 index 0000000..16dac49 --- /dev/null +++ b/web/src/agent/indicators.ts @@ -0,0 +1,119 @@ +import type { IndicatorPoint, PriceCandle } from "../shared/types"; + +export function calculateIndicators(candles: PriceCandle[]): IndicatorPoint[] { + const closes = candles.map((candle) => candle.close); + const ema12 = ema(closes, 12); + const ema26 = ema(closes, 26); + const macdLine = closes.map((_, index) => valueOrUndefined(ema12[index], ema26[index], (fast, slow) => fast - slow)); + const signalLine = ema(macdLine, 9); + const rsi = rsi14(closes); + + return candles.map((candle, index) => { + const macd = macdLine[index]; + const macdSignal = signalLine[index]; + return { + date: candle.date, + ma5: movingAverage(closes, index, 5), + ma20: movingAverage(closes, index, 20), + ma60: movingAverage(closes, index, 60), + ma120: movingAverage(closes, index, 120), + ma200: movingAverage(closes, index, 200), + rsi14: rsi[index], + macd, + macdSignal, + macdHistogram: valueOrUndefined(macd, macdSignal, (line, signal) => line - signal) + }; + }); +} + +function movingAverage(values: number[], index: number, window: number): number | undefined { + if (index + 1 < window) { + return undefined; + } + const slice = values.slice(index + 1 - window, index + 1); + return round(slice.reduce((sum, value) => sum + value, 0) / window, 4); +} + +function ema(values: Array, window: number): Array { + const result: Array = []; + const multiplier = 2 / (window + 1); + let previous: number | undefined; + let seedValues: number[] = []; + + for (const value of values) { + if (value === undefined || !Number.isFinite(value)) { + result.push(undefined); + continue; + } + + if (previous === undefined) { + seedValues.push(value); + if (seedValues.length < window) { + result.push(undefined); + continue; + } + previous = seedValues.reduce((sum, item) => sum + item, 0) / window; + result.push(round(previous, 4)); + continue; + } + + previous = value * multiplier + previous * (1 - multiplier); + result.push(round(previous, 4)); + } + + return result; +} + +function rsi14(closes: number[]): Array { + const result: Array = [undefined]; + let gains = 0; + let losses = 0; + + for (let index = 1; index < closes.length; index += 1) { + const change = closes[index] - closes[index - 1]; + const gain = Math.max(0, change); + const loss = Math.max(0, -change); + + if (index <= 14) { + gains += gain; + losses += loss; + if (index === 14) { + gains /= 14; + losses /= 14; + result.push(rsiValue(gains, losses)); + } else { + result.push(undefined); + } + continue; + } + + gains = (gains * 13 + gain) / 14; + losses = (losses * 13 + loss) / 14; + result.push(rsiValue(gains, losses)); + } + + return result; +} + +function rsiValue(gain: number, loss: number): number { + if (loss === 0) { + return 100; + } + return round(100 - 100 / (1 + gain / loss), 4); +} + +function valueOrUndefined( + left: number | undefined, + right: number | undefined, + calculate: (left: number, right: number) => number +): number | undefined { + if (left === undefined || right === undefined) { + return undefined; + } + return round(calculate(left, right), 4); +} + +function round(value: number, digits: number): number { + const factor = 10 ** digits; + return Math.round(value * factor) / factor; +} diff --git a/web/src/agent/provider.ts b/web/src/agent/provider.ts new file mode 100644 index 0000000..2ea8939 --- /dev/null +++ b/web/src/agent/provider.ts @@ -0,0 +1,1240 @@ +import { z } from "zod"; +import type { + FinancialStatement, + MarketDataProvider, + PriceCandle, + QuantAnalysis, + QuantAnalysisInput, + RelatedNews, + RelatedStockData, + StockSnapshot, + WatchStock +} from "../shared/types"; + +type FetchLike = typeof fetch; +type NasdaqAssetClass = "stocks" | "etf"; +interface NasdaqHistoricalRow { + date: string; + close: string; + volume: string; + open: string; + high: string; + low: string; +} + +interface SinaKlineRow { + day: string; + open: string; + high: string; + low: string; + close: string; + volume: string; +} + +export const IPADS_BASE_URL = "http://tianx.ipads-lab.se.sjtu.edu.cn:8319/v1"; +export const IPADS_WIRE_API = "responses"; +export const IPADS_MODEL = "gpt-5.5"; +const NASDAQ_ASSET_CLASSES: NasdaqAssetClass[] = ["stocks", "etf"]; + +const snapshotSchema = z.object({ + price: z.number(), + currency: z.string().min(1), + pe: z.number().optional(), + forwardPe: z.number().optional(), + revenueGrowth: z.number().optional(), + profitGrowth: z.number().optional(), + nextEarningsDate: z.string().optional(), + peers: z.array(z.object({ display: z.string(), pe: z.number() })).default([]), + news: z.array(z.object({ title: z.string(), url: z.string(), source: z.string().optional(), publishedAt: z.string().optional() })).default([]), + asOf: z.string().optional(), + sources: z.array(z.object({ name: z.string(), url: z.string().optional(), asOf: z.string().optional() })).default([]) +}); + +const financialStatementSchema = z.object({ + period: z.string(), + reportDate: z.string().optional(), + revenue: z.number(), + netIncome: z.number(), + eps: z.number().optional(), + grossMargin: z.number().optional(), + operatingCashFlow: z.number().optional(), + debtToAsset: z.number().optional() +}); + +const fundamentalsSchema = z.array(financialStatementSchema).min(1); + +const relatedDataSchema = z.object({ + sector: z.string().min(1).optional(), + industryRank: z.number().optional(), + beta: z.number().optional(), + avgVolume20: z.number().optional(), + volumeRatio: z.number().optional(), + turnoverRate: z.number().optional(), + volatility20: z.number().optional(), + drawdownFromHigh: z.number().optional(), + relativeStrength60: z.number().optional(), + support: z.number().optional(), + resistance: z.number().optional(), + capitalFlow: z.number().optional(), + sentimentScore: z.number().optional(), + newsHeat: z.number().optional(), + marketCap: z.number().optional(), + enterpriseValue: z.number().optional(), + sharesOutstanding: z.number().optional(), + freeFloat: z.number().optional(), + priceToBook: z.number().optional(), + priceToSales: z.number().optional(), + pegRatio: z.number().optional(), + evToEbitda: z.number().optional(), + dividendYield: z.number().optional(), + freeCashFlowYield: z.number().optional(), + grossMarginTtm: z.number().optional(), + netMarginTtm: z.number().optional(), + roeTtm: z.number().optional(), + roaTtm: z.number().optional(), + debtToEquity: z.number().optional(), + currentRatio: z.number().optional(), + revenueCagr3y: z.number().optional(), + epsGrowthTtm: z.number().optional(), + analystRating: z.string().optional(), + analystTargetPrice: z.number().optional(), + institutionalOwnership: z.number().optional(), + insiderOwnership: z.number().optional(), + shortInterestRatio: z.number().optional(), + nextExDividendDate: z.string().optional(), + indexMembership: z.array(z.string()).default([]), + recentNews: z.array(z.object({ title: z.string(), url: z.string(), source: z.string().optional(), publishedAt: z.string().optional() })).default([]), + socialMentions: z.array(z.object({ title: z.string(), url: z.string(), source: z.string().optional(), publishedAt: z.string().optional() })).default([]), + socialDiscussionCount: z.number().optional(), + webSentimentLabel: z.string().optional(), + riskFlags: z.array(z.string()).default([]), + asOf: z.string().optional(), + sources: z.array(z.object({ name: z.string(), url: z.string().optional(), asOf: z.string().optional() })).default([]) +}); + +const quantAnalysisSchema = z.object({ + action: z.enum(["buy_watch", "hold", "avoid"]), + score: z.number().min(0).max(100), + confidence: z.number().min(0).max(1), + horizon: z.string().min(1), + summary: z.string().min(1), + factors: z + .array( + z.object({ + name: z.string().min(1), + score: z.number().min(0).max(100), + weight: z.number().min(0).max(1), + evidence: z.string().min(1) + }) + ) + .min(3), + tradePlan: z + .object({ + direction: z.enum(["long", "wait", "reduce", "avoid"]), + entryPrice: z.number().optional(), + targetPrice: z.number().optional(), + stopLoss: z.number().optional(), + expectedReturnPct: z.number().optional(), + invalidation: z.string().optional(), + rationale: z.string().min(1) + }) + .optional(), + marketContext: z.array(z.string()).default([]), + riskNotes: z.array(z.string()).default([]) +}); + +const responsesOutputSchema = z.object({ + output_text: z.string().optional(), + output: z + .array( + z.object({ + content: z + .array( + z.object({ + type: z.string().optional(), + text: z.string().optional() + }) + ) + .optional() + }) + ) + .optional() +}); + +export class IpadsResponsesProvider implements MarketDataProvider { + constructor( + private readonly apiKey: string, + private readonly fetchImpl: FetchLike = fetch + ) {} + + async snapshot(stock: WatchStock): Promise { + if (stock.market === "CN") { + return fetchSinaChinaSnapshot(stock, this.fetchImpl); + } + return fetchNasdaqSnapshot(stock, this.fetchImpl); + } + + async candles(stock: WatchStock, startDate?: string): Promise { + if (stock.market === "CN") { + return fetchSinaChinaCandles(stock, this.fetchImpl, startDate); + } + return fetchNasdaqCandles(stock, this.fetchImpl, startDate); + } + + async fundamentals(stock: WatchStock): Promise { + return this.requestJson(buildFundamentalsPrompt(stock), fundamentalsSchema, "fundamentals").then((statements) => + statements.sort((left, right) => right.period.localeCompare(left.period)) + ); + } + + async relatedData(stock: WatchStock): Promise { + if (stock.market === "CN") { + return fetchSinaChinaRelatedData(stock, this.fetchImpl); + } + return fetchNasdaqRelatedData(stock, this.fetchImpl); + } + + async quantAnalysis(input: QuantAnalysisInput): Promise { + try { + return await this.requestJson(buildQuantAnalysisPrompt(input), quantAnalysisSchema, "quant analysis"); + } catch (error) { + return buildRuleBasedQuantAnalysis(input, error instanceof Error ? error.message : "远端 agent 未返回有效结果"); + } + } + + private async requestJson(prompt: string, schema: z.ZodType, context: string): Promise { + const response = await this.fetchImpl(`${IPADS_BASE_URL}/${IPADS_WIRE_API}`, { + method: "POST", + headers: { + Accept: "application/json", + Authorization: `Bearer ${this.apiKey}`, + "Content-Type": "application/json" + }, + signal: AbortSignal.timeout(60_000), + body: JSON.stringify({ + model: IPADS_MODEL, + input: prompt + }) + }); + + if (!response.ok) { + throw new Error(`IPADS Responses request failed: ${response.status}`); + } + + return parseResponsesJson(await response.json(), schema, context); + } +} + +function buildSnapshotPrompt(stock: WatchStock): string { + return [ + "Return only a valid JSON object for a stock monitoring snapshot using verified current market, filing, exchange, or reputable financial data.", + "Use this exact schema:", + '{"price": 100, "currency": "USD", "pe": 20, "forwardPe": 18, "revenueGrowth": 0.08, "profitGrowth": 0.05, "nextEarningsDate": "2026-05-13", "peers": [{"display": "MSFT", "pe": 28}], "news": [{"title": "Earnings preview", "url": "https://example.com/news"}], "asOf": "2026-05-12", "sources": [{"name": "exchange or data vendor", "url": "https://example.com", "asOf": "2026-05-12"}]}', + "Do not infer, fabricate, or fill with approximate values. Required numeric fields must be the latest verified values available from sources. Optional fields may be omitted when not verified.", + `Stock request: ${JSON.stringify({ symbol: stock.display, market: stock.market, name: stock.name })}` + ].join("\n"); +} + +function buildFundamentalsPrompt(stock: WatchStock): string { + return [ + "Return only a valid JSON array of recent financial statements for this stock using verified company filings or reputable financial data providers.", + "Use the latest 8 quarterly or semiannual periods available, sorted newest to oldest.", + "Use this exact item schema:", + '{"period":"2026Q1","reportDate":"2026-04-30","revenue":1000000000,"netIncome":120000000,"eps":1.2,"grossMargin":0.42,"operatingCashFlow":150000000,"debtToAsset":0.34}', + "Do not infer, fabricate, annualize, or estimate fields. Optional fields may be omitted when not verified.", + `Stock request: ${JSON.stringify({ symbol: stock.display, market: stock.market, name: stock.name })}` + ].join("\n"); +} + +function buildRelatedDataPrompt(stock: WatchStock): string { + return [ + "Return only a valid JSON object of verified market, valuation, quality, liquidity, ownership, analyst, and risk context for quantitative stock analysis.", + "Use this exact schema:", + '{"sector":"Technology","industryRank":12,"beta":1.1,"avgVolume20":12000000,"volumeRatio":1.2,"turnoverRate":0.018,"volatility20":0.026,"drawdownFromHigh":0.12,"relativeStrength60":0.08,"support":98,"resistance":115,"capitalFlow":0.03,"sentimentScore":0.62,"newsHeat":0.55,"marketCap":10000000000,"enterpriseValue":11000000000,"sharesOutstanding":1000000000,"freeFloat":0.82,"priceToBook":4.2,"priceToSales":5.1,"pegRatio":1.6,"evToEbitda":18,"dividendYield":0.012,"freeCashFlowYield":0.025,"grossMarginTtm":0.42,"netMarginTtm":0.18,"roeTtm":0.22,"roaTtm":0.11,"debtToEquity":0.35,"currentRatio":1.8,"revenueCagr3y":0.1,"epsGrowthTtm":0.08,"analystRating":"Buy","analystTargetPrice":115,"institutionalOwnership":0.61,"insiderOwnership":0.04,"shortInterestRatio":1.8,"nextExDividendDate":"2026-06-10","indexMembership":["S&P 500"],"riskFlags":["high valuation"],"asOf":"2026-05-12","sources":[{"name":"verified data source","url":"https://example.com","asOf":"2026-05-12"}]}', + "All ratios are decimals, for example 0.12 for 12%. capitalFlow is -1 to 1 if verified. sentimentScore and newsHeat are 0 to 1 only if derived from verified data.", + "Do not infer, fabricate, or approximate. Omit optional fields when not verified. Always include riskFlags and sources arrays, even if empty.", + `Stock request: ${JSON.stringify({ symbol: stock.display, market: stock.market, name: stock.name })}` + ].join("\n"); +} + +function buildQuantAnalysisPrompt(input: QuantAnalysisInput): string { + const compactInput = { + stock: input.stock, + snapshot: input.snapshot, + latestCandles: input.candles.slice(-90), + latestIndicators: input.indicators.slice(-90), + fundamentals: input.fundamentals.slice(0, 8), + relatedData: input.relatedData + }; + return [ + "只返回一个合法 JSON 对象,用中文做标准量化监控分析。这不是个性化投资建议。", + "判断当前数据下这只股票是否值得短期买入观察、继续持有观察,或回避/减仓。", + "必须使用动量、趋势、波动率、流动性、估值、成长质量、资金/舆情、新闻和社交媒体讨论、风险控制进行综合判断。", + "如果输入里有 recentNews、socialMentions、webSentimentLabel、socialDiscussionCount,必须把它们作为市场舆情证据;如果缺失,要在 riskNotes 里说明。", + "不要捏造缺失数据。只能基于提供的已验证输入结论;缺失或过期数据必须降低 confidence 或写入 riskNotes。", + "所有 summary、factor evidence、riskNotes、marketContext、tradePlan.rationale 和 invalidation 都必须是中文。", + "使用这个精确 schema:", + '{"action":"buy_watch","score":72,"confidence":0.68,"horizon":"1-3个月","summary":"趋势和成交量改善,但波动较高,适合小仓位分批观察。","factors":[{"name":"趋势动量","score":75,"weight":0.2,"evidence":"收盘价站上MA20和MA60。"}],"tradePlan":{"direction":"long","entryPrice":18.6,"targetPrice":22.3,"stopLoss":17.2,"expectedReturnPct":0.1989,"invalidation":"跌破17.2且成交量放大。","rationale":"目标价取20日压力位、分析师目标价和波动区间的保守值。"},"marketContext":["近7天新闻热度有限,社交讨论不足。"],"riskNotes":["波动率较高,需控制仓位。"]}', + "action 必须是 buy_watch、hold、avoid 之一。score 0-100。confidence 0-1。factor weights 加总应接近 1。", + "如果 action=buy_watch,tradePlan 必须给出 entryPrice、targetPrice、stopLoss、expectedReturnPct 和开始卖出的价格依据。", + "如果 action=avoid,tradePlan.direction 应为 avoid 或 reduce,并给出短期回避/卖出触发价或风险价位。hold 时 direction 用 wait。", + `Analysis input: ${JSON.stringify(compactInput)}` + ].join("\n"); +} + +export class DemoMarketDataProvider implements MarketDataProvider { + async snapshot(stock: WatchStock): Promise { + const seed = stock.display.split("").reduce((sum, char) => sum + char.charCodeAt(0), 0); + const marketCurrency = stock.market === "US" ? "USD" : stock.market === "HK" ? "HKD" : "CNY"; + const pe = round(10 + (seed % 24), 2); + const peerBase = pe + ((seed % 2 === 0 ? 1 : -1) * (4 + (seed % 6))); + const nextEarnings = new Date(Date.UTC(2026, 4, 11 + (seed % 21))).toISOString().slice(0, 10); + + return { + price: round(20 + (seed % 180) + (seed % 13) / 10, 2), + currency: marketCurrency, + pe, + forwardPe: round(pe * 0.92, 2), + revenueGrowth: round(((seed % 18) - 4) / 100, 4), + profitGrowth: round(((seed % 16) - 6) / 100, 4), + nextEarningsDate: nextEarnings, + peers: [ + { display: peerDisplay(stock, 1), pe: round(peerBase, 2) }, + { display: peerDisplay(stock, 2), pe: round(peerBase + 2.3, 2) } + ], + news: [ + { + title: `${stock.display} related sector signal updated`, + url: `https://example.local/news/${encodeURIComponent(stock.display)}` + } + ], + asOf: new Date().toISOString().slice(0, 10), + sources: [{ name: "demo provider", asOf: new Date().toISOString().slice(0, 10) }] + }; + } + + async candles(stock: WatchStock, startDate?: string): Promise { + const seed = symbolSeed(stock); + const candles: PriceCandle[] = []; + const end = new Date(); + const start = startDate ? new Date(`${startDate}T00:00:00.000Z`) : new Date(Date.UTC(end.getUTCFullYear() - 10, end.getUTCMonth(), end.getUTCDate())); + let close = 26 + (seed % 160); + + for (let index = 0; candles.length < (startDate ? 12 : 2520); index += 1) { + const date = new Date(start.getTime() + index * 24 * 60 * 60 * 1000); + const day = date.getUTCDay(); + if (day === 0 || day === 6) { + continue; + } + + const wave = Math.sin((index + seed) / 7) * 1.9 + Math.cos((index + seed) / 19) * 1.1; + const drift = ((seed % 11) - 4) * 0.015; + const open = close + Math.sin((index + seed) / 5) * 0.9; + close = Math.max(2, open + wave * 0.55 + drift); + const high = Math.max(open, close) + 0.7 + Math.abs(Math.sin(index / 3)) * 1.4; + const low = Math.max(1, Math.min(open, close) - 0.7 - Math.abs(Math.cos(index / 4)) * 1.2); + const volumeBase = 1_200_000 + (seed % 80) * 45_000; + const volume = Math.round(volumeBase * (1 + Math.abs(wave) / 4 + (index % 9) / 18)); + + candles.push({ + date: date.toISOString().slice(0, 10), + open: round(open, 2), + high: round(high, 2), + low: round(low, 2), + close: round(close, 2), + volume + }); + } + + return candles; + } + + async fundamentals(stock: WatchStock): Promise { + const seed = symbolSeed(stock); + const baseRevenue = 1_000_000_000 + (seed % 900) * 4_500_000; + return Array.from({ length: 8 }, (_, index) => { + const quarter = 1 + ((4 - (index % 4)) % 4); + const year = 2026 - Math.floor((index + (quarter === 4 ? 0 : 1)) / 4); + const growth = 1 - index * 0.035 + Math.sin((seed + index) / 3) * 0.025; + const margin = 0.12 + (seed % 18) / 100 + Math.sin(index / 2) * 0.018; + const revenue = Math.round(baseRevenue * growth); + const netIncome = Math.round(revenue * margin); + + return { + period: `${year}Q${quarter}`, + reportDate: `${year}-${String(quarter * 3).padStart(2, "0")}-28`, + revenue, + netIncome, + eps: round(netIncome / (650_000_000 + (seed % 40) * 10_000_000), 2), + grossMargin: round(0.34 + (seed % 16) / 100 + Math.sin(index) * 0.015, 4), + operatingCashFlow: Math.round(netIncome * (1.05 + (index % 3) * 0.08)), + debtToAsset: round(0.24 + (seed % 24) / 100, 4) + }; + }); + } + + async relatedData(stock: WatchStock): Promise { + const seed = symbolSeed(stock); + const snapshot = await this.snapshot(stock); + const candles = await this.candles(stock); + const recent = candles.slice(-60); + const high60 = Math.max(...recent.map((candle) => candle.high)); + const low20 = Math.min(...recent.slice(-20).map((candle) => candle.low)); + const high20 = Math.max(...recent.slice(-20).map((candle) => candle.high)); + const last = candles[candles.length - 1]; + const first60 = recent[0]; + const avgVolume20 = Math.round(candles.slice(-20).reduce((sum, candle) => sum + candle.volume, 0) / 20); + + return { + sector: stock.market === "US" ? "Technology" : stock.market === "HK" ? "Internet & Consumer" : "Large-cap Consumption", + industryRank: 1 + (seed % 30), + beta: round(0.75 + (seed % 70) / 100, 2), + avgVolume20, + volumeRatio: round(last.volume / avgVolume20, 2), + turnoverRate: round(0.006 + (seed % 34) / 1000, 4), + volatility20: round(0.018 + (seed % 28) / 1000, 4), + drawdownFromHigh: round((high60 - last.close) / high60, 4), + relativeStrength60: round((last.close - first60.close) / first60.close - ((seed % 9) - 3) / 100, 4), + support: round(low20, 2), + resistance: round(high20, 2), + capitalFlow: round(((seed % 41) - 20) / 100, 4), + sentimentScore: round(0.42 + (seed % 36) / 100, 4), + newsHeat: round(snapshot.news.length ? 0.52 + (seed % 24) / 100 : 0.22 + (seed % 22) / 100, 4), + riskFlags: seed % 5 === 0 ? ["成交量放大但趋势未确认"] : seed % 7 === 0 ? ["估值波动需要等待财报确认"] : [], + marketCap: Math.round(snapshot.price * (700_000_000 + (seed % 80) * 12_000_000)), + enterpriseValue: Math.round(snapshot.price * (760_000_000 + (seed % 95) * 14_000_000)), + sharesOutstanding: 700_000_000 + (seed % 80) * 12_000_000, + freeFloat: round(0.55 + (seed % 35) / 100, 4), + priceToBook: round(1.2 + (seed % 42) / 10, 2), + priceToSales: round(0.8 + (seed % 50) / 10, 2), + pegRatio: round(0.7 + (seed % 32) / 10, 2), + evToEbitda: round(7 + (seed % 28), 2), + dividendYield: round((seed % 45) / 1000, 4), + freeCashFlowYield: round(0.015 + (seed % 50) / 1000, 4), + grossMarginTtm: round(0.32 + (seed % 28) / 100, 4), + netMarginTtm: round(0.08 + (seed % 20) / 100, 4), + roeTtm: round(0.06 + (seed % 25) / 100, 4), + roaTtm: round(0.03 + (seed % 13) / 100, 4), + debtToEquity: round(0.15 + (seed % 80) / 100, 4), + currentRatio: round(0.9 + (seed % 22) / 10, 2), + revenueCagr3y: round(((seed % 24) - 4) / 100, 4), + epsGrowthTtm: round(((seed % 30) - 8) / 100, 4), + analystRating: seed % 3 === 0 ? "Buy" : seed % 3 === 1 ? "Hold" : "Neutral", + analystTargetPrice: round(last.close * (1.04 + (seed % 18) / 100), 2), + institutionalOwnership: round(0.22 + (seed % 65) / 100, 4), + insiderOwnership: round((seed % 18) / 100, 4), + shortInterestRatio: round(0.5 + (seed % 45) / 10, 2), + indexMembership: stock.market === "US" ? ["Demo large-cap universe"] : [], + recentNews: snapshot.news, + socialMentions: [], + socialDiscussionCount: 0, + webSentimentLabel: snapshot.news.length ? "中性" : undefined, + asOf: new Date().toISOString().slice(0, 10), + sources: [{ name: "demo provider", asOf: new Date().toISOString().slice(0, 10) }] + }; + } + + async quantAnalysis(input: QuantAnalysisInput): Promise { + return buildRuleBasedQuantAnalysis(input); + } +} + +export function createProvider(apiKey: string): MarketDataProvider { + if (apiKey.trim()) { + return new IpadsResponsesProvider(apiKey.trim()); + } + return new DemoMarketDataProvider(); +} + +function parseResponsesJson(payload: unknown, schema: z.ZodType, context: string): T { + const response = responsesOutputSchema.parse(payload); + const text = response.output_text ?? response.output?.flatMap((item) => item.content ?? []).find((content) => content.text)?.text; + if (!text) { + throw new Error(`IPADS Responses payload did not include output text for ${context}`); + } + return schema.parse(JSON.parse(stripJsonFence(text))); +} + +function buildRuleBasedQuantAnalysis(input: QuantAnalysisInput, fallbackReason?: string): QuantAnalysis { + const fallbackPrice = positiveNumber(input.snapshot.price); + const last = input.candles[input.candles.length - 1] ?? { + date: "", + open: fallbackPrice, + high: fallbackPrice, + low: fallbackPrice, + close: fallbackPrice, + volume: 0 + }; + const latest = input.indicators[input.indicators.length - 1] ?? { date: last.date }; + const previous = input.candles[Math.max(0, input.candles.length - 21)] ?? last; + const volumeRatio = input.relatedData.volumeRatio ?? 1; + const volatility20 = input.relatedData.volatility20 ?? estimateVolatility(input.candles.slice(-21)) ?? 0.03; + const sentimentScore = input.relatedData.sentimentScore ?? scoreSentimentLabel(input.relatedData.webSentimentLabel); + const capitalFlow = input.relatedData.capitalFlow ?? 0; + const relativeStrength60 = input.relatedData.relativeStrength60 ?? 0; + const drawdownFromHigh = input.relatedData.drawdownFromHigh ?? 0.15; + const newsHeat = input.relatedData.newsHeat ?? Math.min(1, ((input.relatedData.recentNews?.length ?? 0) + (input.relatedData.socialMentions?.length ?? 0)) / 12); + const recentReturn = safeReturn(last.close, previous.close); + const momentum = clampScore(50 + (recentReturn ?? 0) * 220 + (latest.ma20 && last.close > latest.ma20 ? 12 : input.candles.length ? -8 : 0)); + const pe = input.snapshot.pe; + const valuation = isFiniteNumber(pe) + ? clampScore(62 - Math.max(0, pe - 18) * 1.4 + (input.snapshot.forwardPe && input.snapshot.forwardPe < pe ? 8 : 0)) + : input.relatedData.analystTargetPrice && last.close > 0 + ? clampScore(50 + (safeReturn(input.relatedData.analystTargetPrice, last.close, 0) ?? 0) * 80) + : 45; + const growth = clampScore(50 + (input.snapshot.revenueGrowth ?? 0) * 260 + (input.snapshot.profitGrowth ?? 0) * 320); + const liquidity = clampScore(55 + (volumeRatio - 1) * 18 - volatility20 * 220); + const sentiment = clampScore(40 + sentimentScore * 35 + capitalFlow * 18 + relativeStrength60 * 120 + newsHeat * 8); + const riskPenalty = drawdownFromHigh * 70 + input.relatedData.riskFlags.length * 6 + (input.candles.length < 252 ? 7 : 0); + const score = clampScore(momentum * 0.24 + valuation * 0.2 + growth * 0.22 + liquidity * 0.16 + sentiment * 0.18 - riskPenalty); + const action = score >= 68 ? "buy_watch" : score <= 42 ? "avoid" : "hold"; + const tradePlan = buildTradePlan(action, last.close, input.relatedData, volatility20); + const marketContext = [ + input.relatedData.webSentimentLabel ? `网页/社交舆情标签:${input.relatedData.webSentimentLabel}` : "网页/社交舆情数据不足,未作为强信号。", + `近端新闻 ${input.relatedData.recentNews?.length ?? 0} 条,社交讨论 ${input.relatedData.socialDiscussionCount ?? input.relatedData.socialMentions?.length ?? 0} 条。`, + input.relatedData.analystTargetPrice ? `分析师目标价 ${input.relatedData.analystTargetPrice}。` : "缺少可验证分析师目标价。" + ]; + + return { + action, + score: round(score, 0), + confidence: round(Math.min(0.86, 0.48 + Math.abs(score - 50) / 100 - (fallbackReason ? 0.08 : 0)), 2), + horizon: "1-3个月", + summary: + action === "buy_watch" + ? `趋势、估值/目标价或舆情组合达到买入观察阈值;参考卖出价 ${formatPrice(tradePlan.targetPrice)},预估收益 ${formatPct(tradePlan.expectedReturnPct)}。` + : action === "avoid" + ? `量化信号偏弱或风险惩罚较高,短期应回避/降低仓位;跌破 ${formatPrice(tradePlan.stopLoss)} 后风险进一步放大。` + : `优势信号不足,维持观察;只有重新站上 ${formatPrice(tradePlan.targetPrice)} 附近并放量才考虑提高权重。`, + factors: [ + { name: "趋势动量", score: round(momentum, 0), weight: 0.24, evidence: latest.ma20 ? `收盘价${last.close > latest.ma20 ? "高于" : "低于"}MA20,60日相对强度 ${formatPct(relativeStrength60)}。` : "均线数据不足。" }, + { name: "估值与目标价", score: round(valuation, 0), weight: 0.2, evidence: `PE ${input.snapshot.pe ?? "-"},Forward PE ${input.snapshot.forwardPe ?? "-"},分析师目标价 ${input.relatedData.analystTargetPrice ?? "-"}。` }, + { name: "成长质量", score: round(growth, 0), weight: 0.22, evidence: `收入增速 ${formatPct(input.snapshot.revenueGrowth)},利润增速 ${formatPct(input.snapshot.profitGrowth)},财报期数 ${input.fundamentals.length}。` }, + { name: "流动性与波动", score: round(liquidity, 0), weight: 0.16, evidence: `量比 ${volumeRatio},20日波动 ${formatPct(volatility20)}。` }, + { name: "资金与舆情", score: round(sentiment, 0), weight: 0.18, evidence: `资金流 ${capitalFlow},舆情分 ${sentimentScore},新闻热度 ${newsHeat}。` } + ], + tradePlan, + marketContext, + riskNotes: [ + ...(fallbackReason ? [`远端 agent 未返回有效分析,已使用本地量化规则兜底:${fallbackReason}`] : []), + input.candles.length === 0 + ? "数据源未返回历史K线,技术面已忽略。" + : input.candles.length < 2000 + ? `K线真实历史只有 ${input.candles.length} 根,可能是上市时间较短或数据源覆盖有限。` + : "", + input.fundamentals.length ? "" : "缺少可验证财报数据,基本面权重已降低。", + `距离60日高点回撤 ${formatPct(drawdownFromHigh)}。`, + ...input.relatedData.riskFlags, + "该结论是量化监控输出,不构成个性化投资建议。" + ].filter(Boolean) + }; +} + +function buildTradePlan(action: QuantAnalysis["action"], close: number, relatedData: RelatedStockData, volatility20: number): NonNullable { + if (!isFiniteNumber(close) || close <= 0) { + return { + direction: action === "avoid" ? "avoid" : "wait", + invalidation: "数据源未返回有效价格,无法生成价格触发条件。", + rationale: "当前缺少有效价格和历史K线,交易计划只保留观察结论;需要等待行情源恢复后再计算参考买入、卖出和止损价。" + }; + } + const support = relatedData.support && relatedData.support > 0 ? relatedData.support : close * (1 - Math.max(0.04, volatility20 * 2.2)); + const resistance = relatedData.resistance && relatedData.resistance > close ? relatedData.resistance : close * (1 + Math.max(0.06, volatility20 * 2.8)); + const analystTarget = relatedData.analystTargetPrice && relatedData.analystTargetPrice > close ? relatedData.analystTargetPrice : undefined; + const targetPrice = round(Math.max(resistance, analystTarget ? Math.min(analystTarget, close * 1.35) : resistance), 2); + const stopLoss = round(Math.min(support, close * (1 - Math.max(0.035, volatility20 * 1.7))), 2); + const expectedReturnPct = round((targetPrice - close) / close, 4); + if (action === "buy_watch") { + return { + direction: "long", + entryPrice: round(close, 2), + targetPrice, + stopLoss, + expectedReturnPct, + invalidation: `跌破 ${stopLoss} 且无法快速收回,或放量跌破关键支撑。`, + rationale: `买入后优先参考 ${targetPrice} 附近开始分批止盈;该价位综合了近期压力位、分析师目标价和当前波动区间。` + }; + } + if (action === "avoid") { + return { + direction: "avoid", + entryPrice: round(close, 2), + targetPrice: round(resistance, 2), + stopLoss, + expectedReturnPct, + invalidation: `重新站上 ${round(resistance, 2)} 且量能改善前不提高仓位。`, + rationale: `当前风险收益比不足,短期更适合等待趋势修复;已有仓位可把 ${stopLoss} 作为风险控制参考。` + }; + } + return { + direction: "wait", + entryPrice: round(close, 2), + targetPrice: round(resistance, 2), + stopLoss, + expectedReturnPct: round((resistance - close) / close, 4), + invalidation: `跌破 ${stopLoss} 表示观察假设失效。`, + rationale: `当前不是明确买点,等待突破 ${round(resistance, 2)} 或回踩支撑企稳后再评估。` + }; +} + +function estimateVolatility(candles: PriceCandle[]): number | undefined { + if (candles.length < 3) { + return undefined; + } + const returns = candles.slice(1).map((candle, index) => (candle.close - candles[index].close) / candles[index].close); + const average = returns.reduce((sum, value) => sum + value, 0) / returns.length; + const variance = returns.reduce((sum, value) => sum + (value - average) ** 2, 0) / returns.length; + return round(Math.sqrt(variance), 4); +} + +function safeReturn(current: number | undefined, previous: number | undefined, fallback?: number): number | undefined { + if (!isFiniteNumber(current) || !isFiniteNumber(previous) || previous <= 0) { + return fallback; + } + return (current - previous) / previous; +} + +function positiveNumber(value: number | undefined): number { + return isFiniteNumber(value) && value > 0 ? value : 0; +} + +function scoreSentimentLabel(label: string | undefined): number { + if (!label) { + return 0.5; + } + const normalized = label.toLowerCase(); + if (normalized.includes("bull") || normalized.includes("正") || normalized.includes("积极")) { + return 0.68; + } + if (normalized.includes("bear") || normalized.includes("负") || normalized.includes("谨慎")) { + return 0.35; + } + return 0.5; +} + +async function fetchNasdaqCandles(stock: WatchStock, fetchImpl: FetchLike, startDate?: string): Promise { + if (stock.market !== "US") { + throw new Error(`Nasdaq historical candle source currently supports US symbols only: ${stock.display}`); + } + const to = new Date().toISOString().slice(0, 10); + const from = startDate ?? new Date(Date.UTC(new Date().getUTCFullYear() - 10, new Date().getUTCMonth(), new Date().getUTCDate())).toISOString().slice(0, 10); + for (const assetClass of NASDAQ_ASSET_CLASSES) { + const payload = nasdaqHistoricalSchema.parse( + await fetchNasdaqJson(fetchImpl, `https://api.nasdaq.com/api/quote/${encodeURIComponent(stock.symbol)}/historical?assetclass=${assetClass}&fromdate=${from}&todate=${to}&limit=9999`) + ); + const rows = payload.data?.tradesTable?.rows ?? []; + const candles = rows.map(parseNasdaqRow).filter((candle): candle is PriceCandle => candle !== undefined); + if (candles.length) { + return candles.sort((left, right) => left.date.localeCompare(right.date)); + } + } + return []; +} + +async function fetchNasdaqSnapshot(stock: WatchStock, fetchImpl: FetchLike): Promise { + if (stock.market !== "US") { + throw new Error(`Nasdaq snapshot source currently supports US symbols only: ${stock.display}`); + } + for (const assetClass of NASDAQ_ASSET_CLASSES) { + const [info, summary] = await Promise.all([ + fetchNasdaqJson(fetchImpl, `https://api.nasdaq.com/api/quote/${encodeURIComponent(stock.symbol)}/info?assetclass=${assetClass}`), + fetchNasdaqJson(fetchImpl, `https://api.nasdaq.com/api/quote/${encodeURIComponent(stock.symbol)}/summary?assetclass=${assetClass}`) + ]); + const parsedInfo = nasdaqInfoSchema.parse(info); + const parsedSummary = nasdaqSummarySchema.parse(summary); + const price = parseNasdaqNumber(parsedInfo.data?.primaryData?.lastSalePrice ?? ""); + if (!isFiniteNumber(price) || price <= 0) { + continue; + } + const path = assetClass === "etf" ? "etf" : "stocks"; + return { + price, + currency: "USD", + forwardPe: undefined, + peers: [], + news: (parsedInfo.data?.notifications ?? []).slice(0, 5).map((notification) => ({ + title: notification.headline, + url: `https://www.nasdaq.com/market-activity/${path}/` + encodeURIComponent(stock.symbol.toLowerCase()), + source: "Nasdaq" + })), + asOf: parsedInfo.data?.primaryData?.lastTradeTimestamp ?? new Date().toISOString(), + sources: [ + { name: `Nasdaq ${assetClass} quote info`, url: "https://api.nasdaq.com/api/quote/info", asOf: parsedInfo.data?.primaryData?.lastTradeTimestamp }, + { name: `Nasdaq ${assetClass} summary`, url: "https://api.nasdaq.com/api/quote/summary", asOf: new Date().toISOString().slice(0, 10) } + ] + }; + } + throw new Error(`Nasdaq snapshot did not include a valid positive price for ${stock.display}`); +} + +async function fetchNasdaqRelatedData(stock: WatchStock, fetchImpl: FetchLike): Promise { + if (stock.market !== "US") { + throw new Error(`Nasdaq research data source currently supports US symbols only: ${stock.display}`); + } + const summaryPayload = await fetchNasdaqSummaryWithAssetClass(stock, fetchImpl); + const [summary, analyst, holdings, recentNews, stocktwitsMentions, redditMentions] = await Promise.all([ + Promise.resolve(summaryPayload.payload), + fetchNasdaqJson(fetchImpl, `https://api.nasdaq.com/api/analyst/${encodeURIComponent(stock.symbol)}/targetprice`).catch(() => undefined), + fetchNasdaqJson(fetchImpl, `https://api.nasdaq.com/api/company/${encodeURIComponent(stock.symbol)}/institutional-holdings`).catch(() => undefined), + fetchYahooFinanceNews(stock, fetchImpl).catch(() => []), + fetchStocktwitsMentions(stock, fetchImpl).catch(() => []), + fetchRedditMentions(stock, fetchImpl).catch(() => []) + ]); + const summaryData = nasdaqSummarySchema.parse(summary).data?.summaryData ?? {}; + const analystData = analyst ? nasdaqAnalystSchema.parse(analyst).data?.consensusOverview : undefined; + const holdingsData = holdings ? nasdaqHoldingsSchema.parse(holdings).data?.ownershipSummary : undefined; + const highLow = parseRange(summaryData.FiftTwoWeekHighLow?.value); + const todayRange = parseRange(summaryData.TodayHighLow?.value); + const targetPrice = parseOptionalNumber(summaryData.OneYrTarget?.value) ?? analystData?.priceTarget; + const shareVolume = parseOptionalNumber(summaryData.ShareVolume?.value); + const avgVolume = parseOptionalNumber(summaryData.AverageVolume?.value); + + const socialMentions = [...stocktwitsMentions, ...redditMentions].slice(0, 12); + const webSentimentLabel = inferWebSentimentLabel(stocktwitsMentions, recentNews); + + return { + sector: stringifyOptional(summaryData.Sector?.value), + avgVolume20: avgVolume, + volumeRatio: shareVolume && avgVolume ? round(shareVolume / avgVolume, 4) : undefined, + support: todayRange?.low, + resistance: todayRange?.high, + marketCap: parseOptionalNumber(summaryData.MarketCap?.value), + analystRating: analystConsensusLabel(analystData), + analystTargetPrice: targetPrice, + institutionalOwnership: parsePercent(holdingsData?.SharesOutstandingPCT?.value), + sharesOutstanding: parseSharesOutstanding(holdingsData?.ShareoutstandingTotal?.value), + drawdownFromHigh: calculateDrawdown(highLow?.high, parseOptionalNumber(summaryData.PreviousClose?.value)), + recentNews: recentNews.slice(0, 10), + socialMentions, + socialDiscussionCount: socialMentions.length, + webSentimentLabel, + riskFlags: [], + sources: [ + { name: `Nasdaq ${summaryPayload.assetClass} summary`, url: "https://api.nasdaq.com/api/quote/summary", asOf: new Date().toISOString().slice(0, 10) }, + { name: "Nasdaq analyst targetprice", url: "https://api.nasdaq.com/api/analyst/targetprice", asOf: new Date().toISOString().slice(0, 10) }, + { name: "Nasdaq institutional holdings", url: "https://api.nasdaq.com/api/company/institutional-holdings", asOf: new Date().toISOString().slice(0, 10) }, + { name: "Yahoo Finance RSS", url: "https://feeds.finance.yahoo.com/rss/2.0/headline", asOf: new Date().toISOString().slice(0, 10) }, + { name: "Stocktwits public stream", url: "https://api.stocktwits.com/api/2/streams/symbol", asOf: new Date().toISOString().slice(0, 10) }, + { name: "Reddit public search", url: "https://www.reddit.com/search.json", asOf: new Date().toISOString().slice(0, 10) } + ], + asOf: new Date().toISOString().slice(0, 10) + }; +} + +async function fetchNasdaqSummaryWithAssetClass(stock: WatchStock, fetchImpl: FetchLike): Promise<{ assetClass: NasdaqAssetClass; payload: unknown }> { + let fallback: { assetClass: NasdaqAssetClass; payload: unknown } | undefined; + for (const assetClass of NASDAQ_ASSET_CLASSES) { + const payload = await fetchNasdaqJson(fetchImpl, `https://api.nasdaq.com/api/quote/${encodeURIComponent(stock.symbol)}/summary?assetclass=${assetClass}`); + const summaryData = nasdaqSummarySchema.parse(payload).data?.summaryData ?? {}; + fallback ??= { assetClass, payload }; + if (Object.keys(summaryData).length) { + return { assetClass, payload }; + } + } + return fallback ?? { assetClass: "stocks", payload: {} }; +} + +async function fetchSinaChinaSnapshot(stock: WatchStock, fetchImpl: FetchLike): Promise { + const quote = await fetchSinaChinaQuote(stock, fetchImpl); + return { + price: quote.price, + currency: "CNY", + peers: [], + news: [], + asOf: quote.asOf, + sources: [{ name: "Sina CN quote", url: "https://hq.sinajs.cn/list=", asOf: quote.asOf }] + }; +} + +async function fetchSinaChinaCandles(stock: WatchStock, fetchImpl: FetchLike, startDate?: string): Promise { + const symbol = sinaChinaSymbol(stock); + const text = await fetchText( + fetchImpl, + `https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol=${symbol}&scale=240&ma=no&datalen=2600` + ); + const rows = JSON.parse(text) as SinaKlineRow[] | null; + const start = startDate ? new Date(`${startDate}T00:00:00.000Z`).getTime() : Date.UTC(new Date().getUTCFullYear() - 10, new Date().getUTCMonth(), new Date().getUTCDate()); + return (rows ?? []) + .map(parseSinaKlineRow) + .filter((candle): candle is PriceCandle => candle !== undefined) + .filter((candle) => new Date(`${candle.date}T00:00:00.000Z`).getTime() >= start) + .sort((left, right) => left.date.localeCompare(right.date)); +} + +async function fetchSinaChinaRelatedData(stock: WatchStock, fetchImpl: FetchLike): Promise { + const [quote, candles] = await Promise.all([ + fetchSinaChinaQuote(stock, fetchImpl), + fetchSinaChinaCandles(stock, fetchImpl).catch(() => []) + ]); + const recent = candles.slice(-60); + const last = candles[candles.length - 1]; + const first60 = recent[0]; + const high60 = recent.length ? Math.max(...recent.map((candle) => candle.high)) : undefined; + const low20 = candles.length ? Math.min(...candles.slice(-20).map((candle) => candle.low)) : undefined; + const high20 = candles.length ? Math.max(...candles.slice(-20).map((candle) => candle.high)) : undefined; + const avgVolume20 = candles.length ? Math.round(candles.slice(-20).reduce((sum, candle) => sum + candle.volume, 0) / Math.min(20, candles.length)) : undefined; + const volatility20 = estimateVolatility(candles.slice(-21)); + + return { + sector: "China ETF / A-share", + avgVolume20, + volumeRatio: avgVolume20 && quote.volume ? round(quote.volume / avgVolume20, 4) : undefined, + volatility20, + drawdownFromHigh: high60 && last ? round((high60 - last.close) / high60, 4) : undefined, + relativeStrength60: first60 && last ? round((last.close - first60.close) / first60.close, 4) : undefined, + support: low20, + resistance: high20, + marketCap: quote.marketCap, + riskFlags: [], + asOf: quote.asOf, + sources: [ + { name: "Sina CN quote", url: "https://hq.sinajs.cn/list=", asOf: quote.asOf }, + { name: "Sina CN kline", url: "https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData", asOf: quote.asOf } + ] + }; +} + +const nasdaqHistoricalSchema = z.object({ + data: z + .object({ + tradesTable: z + .object({ + rows: z + .array( + z.object({ + date: z.string(), + close: z.string(), + volume: z.string(), + open: z.string(), + high: z.string(), + low: z.string() + }) + ) + .optional() + }) + .optional() + }) + .nullable() + .optional() +}); + +const nasdaqInfoSchema = z.object({ + data: z + .object({ + primaryData: z + .object({ + lastSalePrice: z.string().optional(), + lastTradeTimestamp: z.string().optional() + }) + .optional(), + notifications: z.array(z.object({ headline: z.string() })).optional() + }) + .nullable() + .optional() +}); + +const nasdaqSummarySchema = z.object({ + data: z + .object({ + summaryData: z.record(z.string(), z.object({ label: z.string().optional(), value: z.union([z.string(), z.number()]).optional() })).optional() + }) + .nullable() + .optional() +}); + +const nasdaqAnalystSchema = z.object({ + data: z + .object({ + consensusOverview: z + .object({ + lowPriceTarget: z.number().optional(), + highPriceTarget: z.number().optional(), + priceTarget: z.number().optional(), + buy: z.number().optional(), + sell: z.number().optional(), + hold: z.number().optional() + }) + .optional() + }) + .nullable() + .optional() +}); + +const nasdaqHoldingsSchema = z.object({ + data: z + .object({ + ownershipSummary: z + .object({ + SharesOutstandingPCT: z.object({ value: z.string().optional() }).optional(), + ShareoutstandingTotal: z.object({ value: z.string().optional() }).optional() + }) + .optional() + }) + .nullable() + .optional() +}); + +const stocktwitsSchema = z.object({ + messages: z + .array( + z.object({ + id: z.number().optional(), + body: z.string().optional(), + created_at: z.string().optional(), + entities: z + .object({ + sentiment: z.object({ basic: z.string().optional() }).nullable().optional() + }) + .optional(), + user: z.object({ username: z.string().optional() }).optional() + }) + ) + .default([]) +}); + +const redditSchema = z.object({ + data: z + .object({ + children: z + .array( + z.object({ + data: z.object({ + title: z.string().optional(), + permalink: z.string().optional(), + created_utc: z.number().optional(), + subreddit: z.string().optional() + }) + }) + ) + .default([]) + }) + .optional() +}); + +async function fetchNasdaqJson(fetchImpl: FetchLike, url: string): Promise { + const response = await fetchImpl(url, { + headers: { + Accept: "application/json, text/plain, */*", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124 Safari/537.36", + Origin: "https://www.nasdaq.com", + Referer: "https://www.nasdaq.com/" + }, + signal: AbortSignal.timeout(15_000) + }); + if (!response.ok) { + throw new Error(`Nasdaq request failed: ${response.status}`); + } + return response.json(); +} + +async function fetchText(fetchImpl: FetchLike, url: string, headers: Record = {}): Promise { + const response = await fetchImpl(url, { + headers: { + Accept: "text/html,application/xml,text/xml,*/*", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124 Safari/537.36", + ...headers + }, + signal: AbortSignal.timeout(15_000) + }); + if (!response.ok) { + throw new Error(`Text request failed: ${response.status}`); + } + return response.text(); +} + +async function fetchGbkText(fetchImpl: FetchLike, url: string, headers: Record = {}): Promise { + const response = await fetchImpl(url, { + headers: { + Accept: "text/plain,*/*", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124 Safari/537.36", + Referer: "https://finance.sina.com.cn/", + ...headers + }, + signal: AbortSignal.timeout(15_000) + }); + if (!response.ok) { + throw new Error(`GBK text request failed: ${response.status}`); + } + return new TextDecoder("gb18030").decode(await response.arrayBuffer()); +} + +async function fetchSinaChinaQuote(stock: WatchStock, fetchImpl: FetchLike): Promise<{ price: number; previousClose?: number; high?: number; low?: number; volume?: number; amount?: number; marketCap?: number; asOf: string }> { + const symbol = sinaChinaSymbol(stock); + const text = await fetchGbkText(fetchImpl, `https://hq.sinajs.cn/list=${symbol}`); + const match = text.match(/="([^"]*)"/); + const fields = match?.[1]?.split(",") ?? []; + const price = parseOptionalNumber(fields[3]); + if (!isFiniteNumber(price)) { + throw new Error(`Sina CN quote did not include a valid price for ${stock.display}`); + } + const date = fields[30]; + const time = fields[31]; + const amount = parseOptionalNumber(fields[9]); + return { + price, + previousClose: parseOptionalNumber(fields[2]), + high: parseOptionalNumber(fields[4]), + low: parseOptionalNumber(fields[5]), + volume: parseOptionalNumber(fields[8]), + amount, + asOf: date && time ? `${date} ${time}` : new Date().toISOString(), + }; +} + +async function fetchYahooFinanceNews(stock: WatchStock, fetchImpl: FetchLike): Promise { + const xml = await fetchText( + fetchImpl, + `https://feeds.finance.yahoo.com/rss/2.0/headline?s=${encodeURIComponent(stock.symbol)}®ion=US&lang=en-US` + ); + return parseRssItems(xml).slice(0, 10).map((item) => ({ + ...item, + source: "Yahoo Finance" + })); +} + +async function fetchStocktwitsMentions(stock: WatchStock, fetchImpl: FetchLike): Promise { + const payload = stocktwitsSchema.parse( + await fetchNasdaqJson(fetchImpl, `https://api.stocktwits.com/api/2/streams/symbol/${encodeURIComponent(stock.symbol)}.json`) + ); + return payload.messages.slice(0, 10).flatMap((message) => { + if (!message.body) { + return []; + } + return [ + { + title: message.body.slice(0, 180), + url: message.id ? `https://stocktwits.com/symbol/${encodeURIComponent(stock.symbol)}/message/${message.id}` : `https://stocktwits.com/symbol/${encodeURIComponent(stock.symbol)}`, + source: message.entities?.sentiment?.basic ? `Stocktwits ${message.entities.sentiment.basic}` : "Stocktwits", + publishedAt: message.created_at + } + ]; + }); +} + +async function fetchRedditMentions(stock: WatchStock, fetchImpl: FetchLike): Promise { + const payload = redditSchema.parse( + await fetchNasdaqJson( + fetchImpl, + `https://www.reddit.com/search.json?q=${encodeURIComponent(`${stock.symbol} stock`)}&sort=new&t=week&limit=8` + ) + ); + return (payload.data?.children ?? []).flatMap((item) => { + const title = item.data.title; + if (!title) { + return []; + } + return [ + { + title, + url: item.data.permalink ? `https://www.reddit.com${item.data.permalink}` : "https://www.reddit.com/search/", + source: item.data.subreddit ? `Reddit r/${item.data.subreddit}` : "Reddit", + publishedAt: item.data.created_utc ? new Date(item.data.created_utc * 1000).toISOString() : undefined + } + ]; + }); +} + +function parseRssItems(xml: string): RelatedNews[] { + const matches = Array.from(xml.matchAll(//gi)); + return matches.flatMap((match) => { + const item = match[0]; + const title = decodeXml(readXmlTag(item, "title")); + const link = decodeXml(readXmlTag(item, "link")); + if (!title || !link) { + return []; + } + return [ + { + title, + url: link, + publishedAt: decodeXml(readXmlTag(item, "pubDate")) || undefined + } + ]; + }); +} + +function readXmlTag(xml: string, tag: string): string { + const match = xml.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i")); + return match?.[1]?.replace(/^$/i, "$1").trim() ?? ""; +} + +function decodeXml(value: string): string { + return value + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, "\"") + .replace(/'/g, "'"); +} + +function inferWebSentimentLabel(socialMentions: RelatedNews[], news: RelatedNews[]): string | undefined { + const text = [...socialMentions, ...news].map((item) => `${item.source ?? ""} ${item.title}`.toLowerCase()).join(" "); + if (!text) { + return undefined; + } + const bullish = (text.match(/bullish|buy|breakout|upgrade|beat|surge|rally|看多|买入/g) ?? []).length; + const bearish = (text.match(/bearish|sell|downgrade|miss|drop|lawsuit|risk|看空|卖出/g) ?? []).length; + if (bullish > bearish + 1) { + return "偏积极"; + } + if (bearish > bullish + 1) { + return "偏谨慎"; + } + return "中性"; +} + +function parseNasdaqRow(row: NasdaqHistoricalRow): PriceCandle | undefined { + const date = parseNasdaqDate(row.date); + const open = parseNasdaqNumber(row.open); + const high = parseNasdaqNumber(row.high); + const low = parseNasdaqNumber(row.low); + const close = parseNasdaqNumber(row.close); + const volume = parseNasdaqNumber(row.volume); + if (!date || !isFiniteNumber(open) || !isFiniteNumber(high) || !isFiniteNumber(low) || !isFiniteNumber(close) || !isFiniteNumber(volume)) { + return undefined; + } + return { + date, + open: round(open, 4), + high: round(high, 4), + low: round(low, 4), + close: round(close, 4), + volume: Math.round(volume) + }; +} + +function parseSinaKlineRow(row: SinaKlineRow): PriceCandle | undefined { + const open = parseOptionalNumber(row.open); + const high = parseOptionalNumber(row.high); + const low = parseOptionalNumber(row.low); + const close = parseOptionalNumber(row.close); + const volume = parseOptionalNumber(row.volume); + if (!row.day || !isFiniteNumber(open) || !isFiniteNumber(high) || !isFiniteNumber(low) || !isFiniteNumber(close) || !isFiniteNumber(volume)) { + return undefined; + } + return { + date: row.day, + open: round(open, 4), + high: round(high, 4), + low: round(low, 4), + close: round(close, 4), + volume: Math.round(volume) + }; +} + +function sinaChinaSymbol(stock: WatchStock): string { + return `${stock.display.endsWith(".SS") ? "sh" : "sz"}${stock.symbol}`; +} + +function parseNasdaqDate(value: string): string | undefined { + const match = value.match(/^(\d{2})\/(\d{2})\/(\d{4})$/); + if (!match) { + return undefined; + } + const [, month, day, year] = match; + return `${year}-${month}-${day}`; +} + +function parseNasdaqNumber(value: string | number): number | undefined { + const number = typeof value === "number" ? value : Number(value.replace(/[$,]/g, "")); + return Number.isFinite(number) ? number : undefined; +} + +function parseOptionalNumber(value: string | number | undefined): number | undefined { + return value !== undefined && value !== "" ? parseNasdaqNumber(value) : undefined; +} + +function stringifyOptional(value: string | number | undefined): string | undefined { + return value === undefined || value === "" ? undefined : String(value); +} + +function parsePercent(value: string | number | undefined): number | undefined { + if (!value) { + return undefined; + } + const number = typeof value === "number" ? value : Number(value.replace(/[%,$,]/g, "")); + return Number.isFinite(number) ? round(number / 100, 4) : undefined; +} + +function parseRange(value: string | number | undefined): { low: number; high: number } | undefined { + if (typeof value !== "string" || !value) { + return undefined; + } + const parts = value.split(/[-/]/).map((part) => parseNasdaqNumber(part.trim())).filter(isFiniteNumber); + if (parts.length < 2) { + return undefined; + } + return { low: Math.min(parts[0], parts[1]), high: Math.max(parts[0], parts[1]) }; +} + +interface AnalystConsensus { + buy?: number; + hold?: number; + sell?: number; +} + +function analystConsensusLabel(consensus: AnalystConsensus | undefined): string | undefined { + if (!consensus) { + return undefined; + } + const buy = consensus.buy ?? 0; + const hold = consensus.hold ?? 0; + const sell = consensus.sell ?? 0; + if (buy > hold && buy > sell) { + return "Buy"; + } + if (sell > buy && sell > hold) { + return "Sell"; + } + return "Hold"; +} + +function parseSharesOutstanding(value: string | undefined): number | undefined { + const sharesMillions = parseOptionalNumber(value); + return sharesMillions === undefined ? undefined : sharesMillions * 1_000_000; +} + +function calculateDrawdown(high: number | undefined, previousClose: number | undefined): number | undefined { + return high && previousClose ? round((high - previousClose) / high, 4) : undefined; +} + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +function stripJsonFence(text: string): string { + const trimmed = text.trim(); + const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/); + return fenced?.[1] ?? trimmed; +} + +function peerDisplay(stock: WatchStock, index: number): string { + if (stock.market === "HK") { + return `${String(9000 + index).padStart(5, "0")}.HK`; + } + if (stock.market === "CN") { + return `${600000 + index}.SS`; + } + return index === 1 ? "MSFT" : "GOOGL"; +} + +function symbolSeed(stock: WatchStock): number { + return stock.display.split("").reduce((sum, char) => sum + char.charCodeAt(0), 0); +} + +function round(value: number, digits: number): number { + const factor = 10 ** digits; + return Math.round(value * factor) / factor; +} + +function clampScore(value: number): number { + return Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 50; +} + +function formatPct(value?: number): string { + return value === undefined ? "-" : `${round(value * 100, 2)}%`; +} + +function formatPrice(value?: number): string { + return value === undefined ? "-" : String(round(value, 2)); +} diff --git a/web/src/agent/report.ts b/web/src/agent/report.ts new file mode 100644 index 0000000..f006236 --- /dev/null +++ b/web/src/agent/report.ts @@ -0,0 +1,144 @@ +import type { + DailyReport, + DailyReportItem, + EarningsSignal, + Market, + MarketDataProvider, + Recommendation, + StockSnapshot, + ValuationSignal, + WatchStock +} from "../shared/types"; + +const DAY_MS = 24 * 60 * 60 * 1000; + +export async function generateDailyReport( + watchlist: WatchStock[], + provider: MarketDataProvider, + now = new Date() +): Promise { + const items = await Promise.all( + watchlist.map(async (stock) => buildReportItem(stock, await provider.snapshot(stock), now)) + ); + + return { + generatedAt: now.toISOString(), + summary: buildSummary(items), + items: items.sort((left, right) => rankItem(right) - rankItem(left)) + }; +} + +function buildReportItem(stock: WatchStock, snapshot: StockSnapshot, now: Date): DailyReportItem { + const earnings = analyzeEarnings(snapshot, now); + const valuation = analyzeValuation(snapshot); + const recommendation = recommend(snapshot, valuation, earnings); + + return { + stock, + snapshot, + earnings, + valuation, + recommendation, + news: snapshot.news + }; +} + +function analyzeEarnings(snapshot: StockSnapshot, now: Date): EarningsSignal { + if (!snapshot.nextEarningsDate) { + return { status: "none" }; + } + + const date = new Date(`${snapshot.nextEarningsDate}T00:00:00.000Z`); + const start = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); + const daysUntil = Math.ceil((date.getTime() - start) / DAY_MS); + + return { + status: daysUntil >= 0 && daysUntil <= 14 ? "soon" : "scheduled", + nextDate: snapshot.nextEarningsDate, + daysUntil + }; +} + +function analyzeValuation(snapshot: StockSnapshot): ValuationSignal { + const stockPe = snapshot.pe; + const peerPes = snapshot.peers.map((peer) => peer.pe).filter((pe) => Number.isFinite(pe) && pe > 0); + if (!Number.isFinite(stockPe) || stockPe === undefined || stockPe <= 0 || peerPes.length === 0) { + return { label: "unknown" }; + } + + const peerAveragePe = round(peerPes.reduce((sum, pe) => sum + pe, 0) / peerPes.length, 2); + const spreadPct = round((stockPe - peerAveragePe) / peerAveragePe, 4); + const label = spreadPct <= -0.15 ? "discount" : spreadPct >= 0.15 ? "premium" : "fair"; + + return { + label, + stockPe, + peerAveragePe, + spreadPct + }; +} + +function recommend(snapshot: StockSnapshot, valuation: ValuationSignal, earnings: EarningsSignal): Recommendation { + const revenueGrowth = snapshot.revenueGrowth ?? 0; + const profitGrowth = snapshot.profitGrowth ?? 0; + const reasons: string[] = []; + + if (valuation.label === "discount") { + reasons.push("估值低于可比股票均值"); + } + if (valuation.label === "premium") { + reasons.push("估值高于可比股票均值"); + } + if (revenueGrowth > 0) { + reasons.push("收入仍在增长"); + } + if (profitGrowth > 0) { + reasons.push("利润仍在增长"); + } + if (earnings.status === "soon") { + reasons.push("未来 14 天内有财报事件"); + } + + if (valuation.label === "discount" && revenueGrowth > 0 && profitGrowth >= 0) { + return { action: "buy_watch", confidence: 0.72, reasons }; + } + + if (valuation.label === "premium" && revenueGrowth < 0 && profitGrowth < 0) { + return { action: "avoid", confidence: 0.68, reasons }; + } + + return { + action: "hold", + confidence: earnings.status === "soon" ? 0.58 : 0.52, + reasons: reasons.length ? reasons : ["等待更明确的基本面或估值信号"] + }; +} + +function buildSummary(items: DailyReportItem[]): string { + const counts = new Map([ + ["CN", 0], + ["HK", 0], + ["US", 0] + ]); + for (const item of items) { + counts.set(item.stock.market, (counts.get(item.stock.market) ?? 0) + 1); + } + + const soon = items.filter((item) => item.earnings.status === "soon").length; + const discounts = items.filter((item) => item.valuation.label === "discount").length; + return `CN: ${counts.get("CN")}; HK: ${counts.get("HK")}; US: ${counts.get("US")}; earnings soon: ${soon}; valuation discounts: ${discounts}`; +} + +function rankItem(item: DailyReportItem): number { + return ( + (item.earnings.status === "soon" ? 4 : 0) + + (item.valuation.label === "discount" ? 3 : 0) + + (item.recommendation.action === "buy_watch" ? 2 : 0) + + item.news.length + ); +} + +function round(value: number, digits: number): number { + const factor = 10 ** digits; + return Math.round(value * factor) / factor; +} diff --git a/web/src/agent/stockDataCache.ts b/web/src/agent/stockDataCache.ts new file mode 100644 index 0000000..77c8f20 --- /dev/null +++ b/web/src/agent/stockDataCache.ts @@ -0,0 +1,380 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { calculateIndicators } from "./indicators"; +import type { + ChartPeriod, + FinancialStatement, + MarketDataProvider, + PriceCandle, + QuantAnalysis, + RelatedStockData, + StockChartData, + StockFundamentalsData, + StockQuantAnalysis, + StockRelatedData, + StockSnapshot, + WatchStock +} from "../shared/types"; + +interface CachedEntry { + cachedAt: string; + data: T; +} + +interface StockCacheFile { + snapshot?: CachedEntry; + candles?: CachedEntry; + candlesWindowYears?: number; + candlesProviderVersion?: number; + fundamentals?: CachedEntry; + relatedData?: CachedEntry; + quantAnalysis?: CachedEntry & { version?: number }; +} + +const SNAPSHOT_TTL_MS = 24 * 60 * 60 * 1000; +const CANDLES_TTL_MS = 24 * 60 * 60 * 1000; +const FUNDAMENTALS_TTL_MS = 7 * 24 * 60 * 60 * 1000; +const RELATED_TTL_MS = 24 * 60 * 60 * 1000; +const ANALYSIS_VERSION = 4; +const CANDLES_WINDOW_YEARS = 10; +const CANDLES_PROVIDER_VERSION = 2; + +export class StockDataCache { + constructor( + private readonly cacheDir: string, + private readonly provider: MarketDataProvider, + private readonly now: () => Date = () => new Date() + ) {} + + async snapshot(stock: WatchStock, force = false): Promise { + const cache = await this.read(stock); + if (!force && cache.snapshot && !isExpired(cache.snapshot.cachedAt, SNAPSHOT_TTL_MS, this.now())) { + return cache.snapshot.data; + } + + const next = await this.provider.snapshot(stock); + await this.write(stock, { ...cache, snapshot: { cachedAt: this.now().toISOString(), data: next } }); + return next; + } + + async chart(stock: WatchStock, period: ChartPeriod = "D", force = false): Promise { + const cache = await this.read(stock); + const existing = cache.candles; + let entry: CachedEntry; + try { + if (!existing || force || cache.candlesWindowYears !== CANDLES_WINDOW_YEARS || cache.candlesProviderVersion !== CANDLES_PROVIDER_VERSION) { + entry = { cachedAt: this.now().toISOString(), data: await this.provider.candles(stock) }; + } else if (isExpired(existing.cachedAt, CANDLES_TTL_MS, this.now())) { + entry = existing; + void this.refreshCandles(stock, existing); + } else { + entry = existing; + } + } catch (error) { + if (existing) { + entry = existing; + } else { + throw error; + } + } + if (entry !== cache.candles) { + await this.write(stock, { ...cache, candles: entry, candlesWindowYears: CANDLES_WINDOW_YEARS, candlesProviderVersion: CANDLES_PROVIDER_VERSION }); + } + + const candles = aggregateCandles(entry.data, period); + return { + stock, + period, + candles, + indicators: calculateIndicators(candles), + cachedAt: entry.cachedAt + }; + } + + private async refreshCandles(stock: WatchStock, existing: CachedEntry): Promise { + try { + const cache = await this.read(stock); + if (cache.candles?.cachedAt !== existing.cachedAt) { + return; + } + const startDate = previousCalendarDate(existing.data[existing.data.length - 1]?.date); + const incremental = startDate ? await this.provider.candles(stock, startDate) : await this.provider.candles(stock); + const entry = { + cachedAt: this.now().toISOString(), + data: incremental.length ? mergeCandles(existing.data, incremental) : existing.data + }; + await this.write(stock, { ...cache, candles: entry, candlesWindowYears: CANDLES_WINDOW_YEARS, candlesProviderVersion: CANDLES_PROVIDER_VERSION }); + } catch { + // Keep stale cache when the upstream market data source is unavailable. + } + } + + async refreshChart(stock: WatchStock, period: ChartPeriod = "D"): Promise { + const cache = await this.read(stock); + const existing = cache.candles; + let entry: CachedEntry; + if (existing && cache.candlesWindowYears === CANDLES_WINDOW_YEARS && cache.candlesProviderVersion === CANDLES_PROVIDER_VERSION) { + const startDate = previousCalendarDate(existing.data[existing.data.length - 1]?.date); + const incremental = startDate ? await this.provider.candles(stock, startDate) : await this.provider.candles(stock); + entry = { + cachedAt: this.now().toISOString(), + data: incremental.length ? mergeCandles(existing.data, incremental) : existing.data + }; + } else { + entry = { cachedAt: this.now().toISOString(), data: await this.provider.candles(stock) }; + } + await this.write(stock, { ...cache, candles: entry, candlesWindowYears: CANDLES_WINDOW_YEARS, candlesProviderVersion: CANDLES_PROVIDER_VERSION }); + + const candles = aggregateCandles(entry.data, period); + return { + stock, + period, + candles, + indicators: calculateIndicators(candles), + cachedAt: entry.cachedAt + }; + } + + async fundamentals(stock: WatchStock, force = false): Promise { + const cache = await this.read(stock); + let entry: CachedEntry; + try { + entry = + !force && cache.fundamentals + ? cache.fundamentals + : { cachedAt: this.now().toISOString(), data: await this.provider.fundamentals(stock) }; + } catch { + entry = cache.fundamentals ?? { cachedAt: this.now().toISOString(), data: [] }; + } + if (entry !== cache.fundamentals) { + await this.write(stock, { ...cache, fundamentals: entry }); + } + + return { + stock, + statements: entry.data, + cachedAt: entry.cachedAt + }; + } + + async relatedData(stock: WatchStock, force = false): Promise { + const cache = await this.read(stock); + let entry: CachedEntry; + try { + entry = + !force && cache.relatedData && hasResearchSchema(cache.relatedData.data) + ? cache.relatedData + : { cachedAt: this.now().toISOString(), data: await this.provider.relatedData(stock) }; + } catch { + entry = cache.relatedData ?? { + cachedAt: this.now().toISOString(), + data: { + riskFlags: ["相关市场数据源暂不可用。"], + sources: [] + } + }; + } + if (entry !== cache.relatedData) { + await this.write(stock, { ...cache, relatedData: entry }); + } + + return { + stock, + data: entry.data, + cachedAt: entry.cachedAt + }; + } + + async quantAnalysis(stock: WatchStock, force = false): Promise { + const cache = await this.read(stock); + const hasTodayAnalysis = + cache.quantAnalysis?.version === ANALYSIS_VERSION && isSameShanghaiDate(cache.quantAnalysis.cachedAt, this.now()); + if (!force && cache.quantAnalysis?.version === ANALYSIS_VERSION) { + return { + stock, + analysis: cache.quantAnalysis.data, + generatedAt: cache.quantAnalysis.cachedAt, + cachedAt: cache.quantAnalysis.cachedAt + }; + } + + const refreshSourceData = force || !hasTodayAnalysis; + const snapshot = await this.snapshot(stock, refreshSourceData); + const chart = await this.chart(stock, "D", refreshSourceData); + const fundamentals = await this.fundamentals(stock, refreshSourceData).catch(() => ({ stock, statements: [], cachedAt: this.now().toISOString() })); + const related = await this.relatedData(stock, refreshSourceData).catch(() => ({ + stock, + data: { riskFlags: ["相关市场数据源暂不可用。"], sources: [] }, + cachedAt: this.now().toISOString() + })); + const generatedAt = this.now().toISOString(); + const analysis = await this.provider.quantAnalysis({ + stock, + snapshot, + candles: chart.candles, + indicators: chart.indicators, + fundamentals: fundamentals.statements, + relatedData: related.data + }); + const entry = { cachedAt: generatedAt, data: analysis, version: ANALYSIS_VERSION }; + await this.write(stock, { ...(await this.read(stock)), quantAnalysis: entry }); + + return { + stock, + analysis, + generatedAt, + cachedAt: entry.cachedAt + }; + } + + async analysisNeedsRefresh(stock: WatchStock): Promise { + const cache = await this.read(stock); + return cache.quantAnalysis?.version !== ANALYSIS_VERSION || !isSameShanghaiDate(cache.quantAnalysis.cachedAt, this.now()); + } + + async hasAnalysis(stock: WatchStock): Promise { + const cache = await this.read(stock); + return cache.quantAnalysis?.version === ANALYSIS_VERSION; + } + + placeholderAnalysis(stock: WatchStock): StockQuantAnalysis { + const generatedAt = this.now().toISOString(); + return { + stock, + generatedAt, + cachedAt: generatedAt, + analysis: { + action: "hold", + score: 50, + confidence: 0.1, + horizon: "后台生成中", + summary: "本地暂无 Agent 分析缓存,已在后台生成最新量化分析。当前先展示缓存数据,避免切换自选股时阻塞界面。", + factors: [ + { name: "缓存状态", score: 50, weight: 1, evidence: "等待后台 Agent 分析写入本地缓存。" }, + { name: "技术面", score: 50, weight: 0, evidence: "K线和指标可先独立查看。" }, + { name: "风险控制", score: 50, weight: 0, evidence: "完整交易计划将在后台分析完成后更新。" } + ], + tradePlan: { + direction: "wait", + rationale: "后台分析完成前不生成买入、卖出或止损价。" + }, + marketContext: ["Agent 分析正在后台生成。"], + riskNotes: ["该占位结果只表示后台任务状态,不代表实际投资判断。"] + } + }; + } + + private async read(stock: WatchStock): Promise { + try { + return JSON.parse(await readFile(this.pathFor(stock), "utf8")) as StockCacheFile; + } catch (error) { + if (isMissingFile(error)) { + return {}; + } + throw error; + } + } + + private async write(stock: WatchStock, cache: StockCacheFile): Promise { + await mkdir(this.cacheDir, { recursive: true }); + await writeFile(this.pathFor(stock), `${JSON.stringify(cache, null, 2)}\n`, "utf8"); + } + + private pathFor(stock: WatchStock): string { + return join(this.cacheDir, `${stock.display.replace(/[^A-Z0-9]/g, "_")}.json`); + } +} + +function isExpired(cachedAt: string, ttlMs: number, now: Date): boolean { + const cachedTime = new Date(cachedAt).getTime(); + return !Number.isFinite(cachedTime) || now.getTime() - cachedTime > ttlMs; +} + +function isMissingFile(error: unknown): boolean { + return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"; +} + +function mergeCandles(existing: PriceCandle[], incoming: PriceCandle[]): PriceCandle[] { + const byDate = new Map(); + for (const candle of existing) { + byDate.set(candle.date, candle); + } + for (const candle of incoming) { + byDate.set(candle.date, candle); + } + return Array.from(byDate.values()).sort((left, right) => left.date.localeCompare(right.date)); +} + +function previousCalendarDate(date: string | undefined): string | undefined { + if (!date) { + return undefined; + } + const value = new Date(`${date}T00:00:00.000Z`); + if (!Number.isFinite(value.getTime())) { + return undefined; + } + value.setUTCDate(value.getUTCDate() - 7); + return value.toISOString().slice(0, 10); +} + +function hasResearchSchema(data: RelatedStockData): boolean { + return Array.isArray(data.sources) && data.sources.some((source) => source.name.startsWith("Nasdaq") || source.name.startsWith("Sina")); +} + +function isSameShanghaiDate(cachedAt: string, now: Date): boolean { + const cachedTime = new Date(cachedAt); + if (!Number.isFinite(cachedTime.getTime())) { + return false; + } + return shanghaiDate(cachedTime) === shanghaiDate(now); +} + +function shanghaiDate(date: Date): string { + return new Intl.DateTimeFormat("en-CA", { + timeZone: "Asia/Shanghai", + year: "numeric", + month: "2-digit", + day: "2-digit" + }).format(date); +} + +function aggregateCandles(candles: PriceCandle[], period: ChartPeriod): PriceCandle[] { + if (period === "D") { + return candles; + } + + const grouped = new Map(); + for (const candle of candles) { + const key = periodKey(candle.date, period); + const existing = grouped.get(key); + if (!existing) { + grouped.set(key, { ...candle }); + continue; + } + existing.high = Math.max(existing.high, candle.high); + existing.low = Math.min(existing.low, candle.low); + existing.close = candle.close; + existing.volume += candle.volume; + existing.date = candle.date; + } + return Array.from(grouped.values()); +} + +function periodKey(date: string, period: ChartPeriod): string { + if (period === "M") { + return date.slice(0, 7); + } + if (period === "Y") { + return date.slice(0, 4); + } + return isoWeekKey(date); +} + +function isoWeekKey(date: string): string { + const value = new Date(`${date}T00:00:00.000Z`); + const day = value.getUTCDay() || 7; + value.setUTCDate(value.getUTCDate() + 4 - day); + const yearStart = new Date(Date.UTC(value.getUTCFullYear(), 0, 1)); + const week = Math.ceil(((value.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); + return `${value.getUTCFullYear()}-W${String(week).padStart(2, "0")}`; +} diff --git a/web/src/agent/symbols.ts b/web/src/agent/symbols.ts new file mode 100644 index 0000000..4ecc0f6 --- /dev/null +++ b/web/src/agent/symbols.ts @@ -0,0 +1,52 @@ +import type { Market, NormalizedSymbol } from "../shared/types"; + +const CN_SUFFIXES = new Set(["SS", "SH", "SZ"]); + +export function normalizeSymbol(input: string): NormalizedSymbol { + const raw = input.trim().toUpperCase(); + if (!raw) { + throw new Error("Stock symbol is required"); + } + + const suffixMatch = raw.match(/^([A-Z0-9]+)\.([A-Z]{2})$/); + if (suffixMatch) { + const [, base, suffix] = suffixMatch; + if (CN_SUFFIXES.has(suffix)) { + const exchange = inferChinaExchange(base); + return { + symbol: base, + market: "CN", + display: `${base}.${exchange}` + }; + } + if (suffix === "HK" && /^\d{4,5}$/.test(base)) { + return { symbol: base.padStart(5, "0"), market: "HK", display: `${base.padStart(5, "0")}.HK` }; + } + if (suffix === "US" && /^[A-Z]{1,6}$/.test(base)) { + return { symbol: base, market: "US", display: base }; + } + } + + if (/^\d{6}$/.test(raw)) { + return { symbol: raw, market: "CN", display: `${raw}.${inferChinaExchange(raw)}` }; + } + + if (/^\d{4,5}$/.test(raw)) { + const symbol = raw.padStart(5, "0"); + return { symbol, market: "HK", display: `${symbol}.HK` }; + } + + if (/^[A-Z]{1,6}$/.test(raw)) { + return { symbol: raw, market: "US", display: raw }; + } + + throw new Error("Unsupported stock symbol"); +} + +function inferChinaExchange(symbol: string): "SS" | "SZ" { + return symbol.startsWith("5") || symbol.startsWith("6") || symbol.startsWith("9") ? "SS" : "SZ"; +} + +export function marketLabel(market: Market): string { + return market === "CN" ? "A股" : market === "HK" ? "港股" : "美股"; +} diff --git a/web/src/agent/watchlistStore.ts b/web/src/agent/watchlistStore.ts new file mode 100644 index 0000000..b79ece0 --- /dev/null +++ b/web/src/agent/watchlistStore.ts @@ -0,0 +1,96 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import { normalizeSymbol } from "./symbols"; +import type { WatchStock } from "../shared/types"; + +export class WatchlistStore { + constructor(private readonly path: string) {} + + async list(): Promise { + return this.read(); + } + + async add(symbolInput: string, name?: string, tags: string[] = []): Promise { + const normalized = normalizeSymbol(symbolInput); + const stocks = await this.read(); + const existing = stocks.find((stock) => stock.display === normalized.display); + if (existing) { + return existing; + } + + const stock: WatchStock = { + ...normalized, + id: normalized.display, + name: name?.trim() || undefined, + tags: normalizeTags(tags), + addedAt: new Date().toISOString() + }; + await this.write([...stocks, stock]); + return stock; + } + + async updateTags(symbolInput: string, tags: string[]): Promise { + const normalized = normalizeSymbol(symbolInput); + const stocks = await this.read(); + const stock = stocks.find((item) => item.display === normalized.display); + if (!stock) { + throw new Error("Stock is not in watchlist"); + } + + const updated = { ...stock, tags: normalizeTags(tags) }; + await this.write(stocks.map((item) => (item.display === normalized.display ? updated : item))); + return updated; + } + + async remove(symbolInput: string): Promise { + const normalized = normalizeSymbol(symbolInput); + const stocks = await this.read(); + await this.write(stocks.filter((stock) => stock.display !== normalized.display)); + } + + private async read(): Promise { + try { + const content = await readFile(this.path, "utf8"); + const parsed = JSON.parse(content) as WatchStock[]; + if (!Array.isArray(parsed)) { + return []; + } + const byDisplay = new Map(); + for (const stock of parsed) { + const normalized = normalizeSymbol(stock.display ?? stock.symbol); + byDisplay.set(normalized.display, { + ...stock, + ...normalized, + id: normalized.display, + tags: normalizeTags(stock.tags ?? []) + }); + } + return Array.from(byDisplay.values()); + } catch (error) { + if (isMissingFile(error)) { + return []; + } + throw error; + } + } + + private async write(stocks: WatchStock[]): Promise { + await mkdir(dirname(this.path), { recursive: true }); + await writeFile(this.path, `${JSON.stringify(stocks, null, 2)}\n`, "utf8"); + } +} + +function isMissingFile(error: unknown): boolean { + return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"; +} + +function normalizeTags(tags: string[]): string[] { + return Array.from( + new Set( + tags + .map((tag) => tag.trim()) + .filter(Boolean) + .map((tag) => tag.slice(0, 24)) + ) + ).slice(0, 8); +} diff --git a/web/src/client/LivePage.tsx b/web/src/client/LivePage.tsx new file mode 100644 index 0000000..01f05e2 --- /dev/null +++ b/web/src/client/LivePage.tsx @@ -0,0 +1,686 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { + ColorType, + CrosshairMode, + LineSeries, + createChart, + type IChartApi, + type LineData, + type Time, +} from "lightweight-charts"; +import { ArrowDownRight, ArrowUpRight, RefreshCw, Wallet } from "lucide-react"; +import { + calendarDaysSince, + currencyForMarket, + formatMoney, + formatNumber, + formatPct, + type EquityPoint, + type LiveStrategyState, + type StrategyCurrency, + type StrategyMarket, + type TradeLogEntry, +} from "../shared/strategyTypes"; + +// A strategy whose state was last updated more than this many calendar days +// ago is flagged (covers a normal weekend plus a holiday). +const LIVE_STALE_DAYS = 4; + +async function api(path: string, init?: RequestInit): Promise { + const response = await fetch(path, { + ...init, + headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, + }); + if (!response.ok) { + const body = (await response.json().catch(() => undefined)) as { error?: string } | undefined; + throw new Error(body?.error ?? `Request failed: ${response.status}`); + } + return (await response.json()) as T; +} + +type SortKey = "currentValue" | "unrealizedPnlPct" | "name" | "lastUpdated"; + +export function LivePage() { + const [market, setMarket] = useState("us"); + const [strategies, setStrategies] = useState([]); + const [selectedName, setSelectedName] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [sortKey, setSortKey] = useState("unrealizedPnlPct"); + const [filterText, setFilterText] = useState(""); + + const refresh = (mk: StrategyMarket = market) => { + setLoading(true); + setError(""); + api(`/api/strategies/live?market=${mk}`) + .then((res) => { + setStrategies(res); + if (res.length > 0) { + setSelectedName((prev) => + prev && res.some((s) => s.name === prev) ? prev : res[0].name, + ); + } else { + setSelectedName(null); + } + }) + .catch((err) => setError(err instanceof Error ? err.message : "加载失败")) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + refresh(market); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [market]); + + const sorted = useMemo(() => { + const list = [...strategies]; + const ft = filterText.trim().toLowerCase(); + const filtered = ft + ? list.filter( + (s) => + s.name.toLowerCase().includes(ft) || + s.label.toLowerCase().includes(ft), + ) + : list; + filtered.sort((a, b) => { + switch (sortKey) { + case "currentValue": + return (b.currentValue ?? 0) - (a.currentValue ?? 0); + case "unrealizedPnlPct": + return (b.unrealizedPnlPct ?? 0) - (a.unrealizedPnlPct ?? 0); + case "name": + return a.label.localeCompare(b.label); + case "lastUpdated": + return (b.lastUpdated ?? "").localeCompare(a.lastUpdated ?? ""); + } + }); + return filtered; + }, [strategies, sortKey, filterText]); + + const focused = useMemo( + () => sorted.find((s) => s.name === selectedName) ?? sorted[0], + [sorted, selectedName], + ); + + return ( +
+ {error &&
{error}
} + +
+
+ + +
+ setFilterText(e.target.value)} + /> + + +
+ +
+ + +
+ {focused ? ( + + ) : ( +
+ +

未选择策略

+
+ )} +
+
+
+ ); +} + +function LiveStrategyDetail({ strategy }: { strategy: LiveStrategyState }) { + const m = strategy; + const currency = currencyOf(m); + const staleDays = calendarDaysSince(m.lastUpdated); + const bench = benchmarkComparison(m); + return ( + <> +
+
+
+

{m.name}

+

{m.label}

+

+ {m.market.toUpperCase()} · {currency} · 初始 {money(m.initialCapital, currency)} · 起始 {m.startDate ?? "—"} + {" "}· 更新于 {m.lastUpdated ?? "-"} · 价格截至 {m.priceAsOf ?? "—"} + {isStale(m) && 已 {staleDays} 天未更新} +

+
+
= 0 ? "positive" : "negative"}`}> + {money(m.currentValue, currency)} + + {m.unrealizedPnl >= 0 ? "+" : ""} + {money(m.unrealizedPnl, currency)} ({pct(m.unrealizedPnlPct)}) + +
+
+
+ + + + + + {m.metrics && } + {m.metrics && } + {m.metrics && } + {bench ? ( + <> + + 0} + negative={bench.excessReturn < 0} + /> + + ) : ( + + )} +
+
+ + + + + + + + ); +} + +function currencyOf(s: LiveStrategyState): StrategyCurrency { + return s.currency ?? currencyForMarket(s.market); +} + +function isStale(s: LiveStrategyState): boolean { + const days = calendarDaysSince(s.lastUpdated); + return days !== null && days > LIVE_STALE_DAYS; +} + +// Strategy vs. benchmark over the benchmark's window. The benchmark is rebased +// to initialCapital, so both returns share the same starting capital. +function benchmarkComparison(s: LiveStrategyState) { + const curve = s.benchmark?.equityCurve ?? []; + const first = curve[0]?.value; + const last = curve.at(-1); + if (!last || !first || !s.initialCapital || s.equityCurve.length === 0) return null; + const strategyEnd = s.equityCurve.find((p) => p.date === last.date) ?? s.equityCurve.at(-1)!; + const benchmarkReturn = last.value / first - 1; + const strategyReturn = strategyEnd.value / s.initialCapital - 1; + if (!Number.isFinite(benchmarkReturn) || !Number.isFinite(strategyReturn)) return null; + return { + name: s.benchmark?.name ?? "基准", + benchmarkReturn, + excessReturn: strategyReturn - benchmarkReturn, + }; +} + +function SummaryCell({ + label, + value, + positive, + negative, +}: { + label: string; + value: string; + positive?: boolean; + negative?: boolean; +}) { + const cls = positive ? "positive" : negative ? "negative" : ""; + return ( +
+ {value} + {label} +
+ ); +} + +function LiveEquityChart({ + equityCurve, + initialCapital, + currency, + benchmark, +}: { + equityCurve: EquityPoint[]; + initialCapital: number; + currency: StrategyCurrency; + benchmark?: { name: string; equityCurve: EquityPoint[] }; +}) { + const containerRef = useRef(null); + const chartRef = useRef(null); + + useEffect(() => { + if (!containerRef.current) return; + const chart = createChart(containerRef.current, { + width: containerRef.current.clientWidth, + height: 280, + layout: { + background: { type: ColorType.Solid, color: "#0d1219" }, + textColor: "#b0b0b0", + attributionLogo: false, + }, + grid: { + vertLines: { color: "#1a2030" }, + horzLines: { color: "#1a2030" }, + }, + crosshair: { mode: CrosshairMode.Normal }, + timeScale: { borderColor: "#1d2530", timeVisible: false }, + rightPriceScale: { borderColor: "#1d2530" }, + localization: { priceFormatter: (price: number) => formatMoney(price, currency) }, + }); + chartRef.current = chart; + + if (benchmark && benchmark.equityCurve.length > 0) { + const benchSeries = chart.addSeries(LineSeries, { + color: "#7c8694", + lineWidth: 1, + lineStyle: 2, + title: benchmark.name, + crosshairMarkerVisible: false, + priceLineVisible: false, + }); + benchSeries.setData( + benchmark.equityCurve.map((p) => ({ time: p.date as Time, value: p.value })), + ); + } + + const eqSeries = chart.addSeries(LineSeries, { + color: "#4db6ff", + lineWidth: 2, + priceLineVisible: false, + }); + const data: LineData