deploy: publish THEGOD static frontend
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +0 -9
- assets/{index-DKO8x-qK.js → index-Bh0tM6mi.js} +0 -0
- bun.lock +0 -831
- firebase-applet-config.json +0 -11
- index.html +1 -1
- metadata.json +0 -7
- package.json +0 -36
- server.cjs +0 -0
- server.cjs.map +0 -0
- server.ts +0 -40
- server/ai/quantAnalyst.ts +0 -42
- server/backtest/historicalBacktest.ts +0 -104
- server/backtest/historicalDataService.ts +0 -192
- server/confluence/confluenceEngine.ts +0 -123
- server/confluence/index.ts +0 -2
- server/confluence/marketAnalysisService.ts +0 -60
- server/derivatives/derivativesAnalysis.ts +0 -46
- server/derivatives/derivativesClient.ts +0 -116
- server/derivatives/historicalFundingService.ts +0 -151
- server/derivatives/index.ts +0 -2
- server/divergence/divergenceEngine.ts +0 -99
- server/divergence/index.ts +0 -2
- server/gann/gannEngine.ts +0 -61
- server/gann/index.ts +0 -2
- server/indicators/index.ts +0 -2
- server/indicators/technicalIndicators.ts +0 -103
- server/market/OKX_FIX_PENDING.md +0 -1
- server/market/candleService.ts +0 -78
- server/market/exchangeClient.okx-fix-notes.md +0 -1
- server/market/exchangeClient.ts +0 -123
- server/market/marketCache.ts +0 -61
- server/market/marketSnapshotService.ts +0 -19
- server/market/timeframeService.ts +0 -40
- server/paper/paperTradingEngine.ts +0 -32
- server/paper/paperTradingLoop.ts +0 -25
- server/quant/monteCarloAnalysis.ts +0 -116
- server/quant/multiRegimeAnalytics.ts +0 -223
- server/quant/parameterValidation.ts +0 -75
- server/quant/quantitativeLab.ts +0 -31
- server/quant/regimeAnalytics.ts +0 -27
- server/quant/robustParameterSelection.ts +0 -47
- server/quant/robustnessAnalysis.ts +0 -82
- server/quant/statisticalAnalysis.ts +0 -137
- server/quant/stressTest.ts +0 -60
- server/quant/timeSeriesAnalysis.ts +0 -64
- server/quant/walkForwardAnalysis.ts +0 -113
- server/realMarketData.ts +0 -1018
- server/signal/index.ts +0 -1
- server/signal/signalEngine.ts +0 -188
- server/signal/signalRoute.ts +0 -28
.env.example
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
| 2 |
-
# AI Studio automatically injects this at runtime from user secrets.
|
| 3 |
-
# Users configure this via the Secrets panel in the AI Studio UI.
|
| 4 |
-
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
| 5 |
-
|
| 6 |
-
# APP_URL: The URL where this applet is hosted.
|
| 7 |
-
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
| 8 |
-
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
| 9 |
-
APP_URL="MY_APP_URL"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/{index-DKO8x-qK.js → index-Bh0tM6mi.js}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bun.lock
DELETED
|
@@ -1,831 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"lockfileVersion": 2,
|
| 3 |
-
"configVersion": 1,
|
| 4 |
-
"workspaces": {
|
| 5 |
-
"": {
|
| 6 |
-
"name": "react-example",
|
| 7 |
-
"dependencies": {
|
| 8 |
-
"@google/genai": "^2.4.0",
|
| 9 |
-
"@tailwindcss/vite": "^4.1.14",
|
| 10 |
-
"@vitejs/plugin-react": "^5.0.4",
|
| 11 |
-
"dotenv": "^17.2.3",
|
| 12 |
-
"express": "^4.21.2",
|
| 13 |
-
"firebase": "^12.18.0",
|
| 14 |
-
"lucide-react": "^0.546.0",
|
| 15 |
-
"motion": "^12.23.24",
|
| 16 |
-
"react": "^19.0.1",
|
| 17 |
-
"react-dom": "^19.0.1",
|
| 18 |
-
"vite": "^6.2.3",
|
| 19 |
-
},
|
| 20 |
-
"devDependencies": {
|
| 21 |
-
"@types/express": "^4.17.21",
|
| 22 |
-
"@types/node": "^22.14.0",
|
| 23 |
-
"autoprefixer": "^10.4.21",
|
| 24 |
-
"esbuild": "^0.25.0",
|
| 25 |
-
"tailwindcss": "^4.1.14",
|
| 26 |
-
"tsx": "^4.21.0",
|
| 27 |
-
"typescript": "~5.8.2",
|
| 28 |
-
"vite": "^6.2.3",
|
| 29 |
-
},
|
| 30 |
-
},
|
| 31 |
-
},
|
| 32 |
-
"packages": {
|
| 33 |
-
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
| 34 |
-
|
| 35 |
-
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
| 36 |
-
|
| 37 |
-
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@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" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
| 38 |
-
|
| 39 |
-
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
| 40 |
-
|
| 41 |
-
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
| 42 |
-
|
| 43 |
-
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
| 44 |
-
|
| 45 |
-
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
| 46 |
-
|
| 47 |
-
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
| 48 |
-
|
| 49 |
-
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
| 50 |
-
|
| 51 |
-
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
| 52 |
-
|
| 53 |
-
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
| 54 |
-
|
| 55 |
-
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
| 56 |
-
|
| 57 |
-
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
| 58 |
-
|
| 59 |
-
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
| 60 |
-
|
| 61 |
-
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
| 62 |
-
|
| 63 |
-
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
| 64 |
-
|
| 65 |
-
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
| 66 |
-
|
| 67 |
-
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
| 68 |
-
|
| 69 |
-
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
| 70 |
-
|
| 71 |
-
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
| 72 |
-
|
| 73 |
-
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
| 74 |
-
|
| 75 |
-
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
| 76 |
-
|
| 77 |
-
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
| 78 |
-
|
| 79 |
-
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
| 80 |
-
|
| 81 |
-
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
| 82 |
-
|
| 83 |
-
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
| 84 |
-
|
| 85 |
-
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
| 86 |
-
|
| 87 |
-
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
| 88 |
-
|
| 89 |
-
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
| 90 |
-
|
| 91 |
-
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
| 92 |
-
|
| 93 |
-
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
| 94 |
-
|
| 95 |
-
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
| 96 |
-
|
| 97 |
-
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
| 98 |
-
|
| 99 |
-
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
| 100 |
-
|
| 101 |
-
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
| 102 |
-
|
| 103 |
-
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
| 104 |
-
|
| 105 |
-
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
| 106 |
-
|
| 107 |
-
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
| 108 |
-
|
| 109 |
-
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
| 110 |
-
|
| 111 |
-
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
| 112 |
-
|
| 113 |
-
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
| 114 |
-
|
| 115 |
-
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
| 116 |
-
|
| 117 |
-
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
| 118 |
-
|
| 119 |
-
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
| 120 |
-
|
| 121 |
-
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
| 122 |
-
|
| 123 |
-
"@firebase/ai": ["@firebase/ai@2.15.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.5", "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-types": "0.x" } }, "sha512-Aj7TbFdAIWZdkX8JfdDStERpR35g6WNs+7XhNPtFLFOizUotj6k4N/D8HJkR45165HhnMJBe4hjOeeaxnnn56Q=="],
|
| 124 |
-
|
| 125 |
-
"@firebase/analytics": ["@firebase/analytics@0.10.24", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/installations": "0.6.24", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-OfIAcIIwoqXjBzS+DnUQpOZ7i3ePaNFg83KPpDWjBZUKJmqMwzzAtLJqmKZOkQnW8im6vFR6f2I3M/9hxVd+QA=="],
|
| 126 |
-
|
| 127 |
-
"@firebase/analytics-compat": ["@firebase/analytics-compat@0.2.30", "", { "dependencies": { "@firebase/analytics": "0.10.24", "@firebase/analytics-types": "0.8.5", "@firebase/component": "0.7.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-uVZEKlLaW4AHAhv8zoN9cTmveX6v86AVqcJ4LCaRCMTChTH8/NsjbisTq1lpBfWCLWS1spqwSHB4vol/YSCdMA=="],
|
| 128 |
-
|
| 129 |
-
"@firebase/analytics-types": ["@firebase/analytics-types@0.8.5", "", {}, "sha512-kdnooE7Bis2jEnsqcerRwn/UQpH5D3uvHpku7OdUM9TJN3omlu6iYtbyAQ6XkAJRgJtO0aNf9OOkW8J6D59oCA=="],
|
| 130 |
-
|
| 131 |
-
"@firebase/app": ["@firebase/app@0.16.1", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "idb": "7.1.1", "tslib": "^2.1.0" } }, "sha512-tjUEorFyKrurH7PbLWv9zDHRuU4mLefgD/yY38D584ziorIRlQz/PVjx4c/SCGyCuUlmyzt72mK+Lh+COAywNQ=="],
|
| 132 |
-
|
| 133 |
-
"@firebase/app-check": ["@firebase/app-check@0.13.1", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-l8y3dmnhodXks/APAwx4tWqRl3tk8b9874KF1FJyKg1DIU+kMD0l52iz7S9/MW+eK8k6cjJg0G0iJ5KQAsQpow=="],
|
| 134 |
-
|
| 135 |
-
"@firebase/app-check-compat": ["@firebase/app-check-compat@0.4.7", "", { "dependencies": { "@firebase/app-check": "0.13.1", "@firebase/app-check-types": "0.5.5", "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-fBb/xSMyIKv1nDmccfzz3IAGBzx/cQOxmZai66rJzifb1hMMkztf824Xx7LhpTUe7HNUbXwY90IvJ66fi8q6yg=="],
|
| 136 |
-
|
| 137 |
-
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.5", "", {}, "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg=="],
|
| 138 |
-
|
| 139 |
-
"@firebase/app-check-types": ["@firebase/app-check-types@0.5.5", "", {}, "sha512-+DF4gzFrlwGFyky38o4T/YN/r51l70yCtJ2HTkwmRj8FCMwPjm4hLH4fQbj6BUvzkiFqliBkitFsRpf1/YZkWA=="],
|
| 140 |
-
|
| 141 |
-
"@firebase/app-compat": ["@firebase/app-compat@0.5.17", "", { "dependencies": { "@firebase/app": "0.16.1", "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" } }, "sha512-5GdJWobqs6jbNYOnaQiSa4Ng8gnFerhqr7nY+v5jlnT7o9QbEeIZRLPQpnLe0TxYSWfRV2lAMbyR0kbXeIos9Q=="],
|
| 142 |
-
|
| 143 |
-
"@firebase/app-types": ["@firebase/app-types@0.9.6", "", { "dependencies": { "@firebase/logger": "0.5.2" } }, "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg=="],
|
| 144 |
-
|
| 145 |
-
"@firebase/auth": ["@firebase/auth@1.13.5", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" }, "optionalPeers": ["@react-native-async-storage/async-storage"] }, "sha512-1AXoBJqBVD8WL8FZYo3S2GmJF9YUoom6Y6ngMxOSkzzhW5sT83pLchb6TGFgxes91dfXx8s/VYc5VrLDNqpLog=="],
|
| 146 |
-
|
| 147 |
-
"@firebase/auth-compat": ["@firebase/auth-compat@0.6.10", "", { "dependencies": { "@firebase/auth": "1.13.5", "@firebase/auth-types": "0.13.2", "@firebase/component": "0.7.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-Bvklg2nL7BrBFCAqdsleW8fse9Jh0fARqJPlJmA40uermfWx1bJiO7dnKyZN3J39d2TFVd4VJXtg3i6Wg92/YQ=="],
|
| 148 |
-
|
| 149 |
-
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.6", "", {}, "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg=="],
|
| 150 |
-
|
| 151 |
-
"@firebase/auth-types": ["@firebase/auth-types@0.13.2", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-OU+miuoSxIWYN7GT291d2ylVJBL3k/OePo7/JDSoQtkFQoEiGBc4AHuMjj/seqFIsHrqnjrAfRCk4pfufoJolQ=="],
|
| 152 |
-
|
| 153 |
-
"@firebase/component": ["@firebase/component@0.7.5", "", { "dependencies": { "@firebase/util": "1.15.3", "tslib": "^2.1.0" } }, "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw=="],
|
| 154 |
-
|
| 155 |
-
"@firebase/data-connect": ["@firebase/data-connect@0.7.4", "", { "dependencies": { "@firebase/auth-interop-types": "0.2.6", "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-su1aGWlzhxb+xtggCUSsufJn1FDa06SBDK71y+fpQ+g2zMhMExik6FUv/odkKzOF/xfWVjabCbWBcjJY3MceDA=="],
|
| 156 |
-
|
| 157 |
-
"@firebase/database": ["@firebase/database@1.1.5", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.5", "@firebase/auth-interop-types": "0.2.6", "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ=="],
|
| 158 |
-
|
| 159 |
-
"@firebase/database-compat": ["@firebase/database-compat@2.1.7", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/database": "1.1.5", "@firebase/database-types": "1.0.22", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" }, "optionalPeers": ["@firebase/app", "@firebase/app-compat"] }, "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w=="],
|
| 160 |
-
|
| 161 |
-
"@firebase/database-types": ["@firebase/database-types@1.0.22", "", { "dependencies": { "@firebase/app-types": "0.9.6", "@firebase/util": "1.15.3" } }, "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug=="],
|
| 162 |
-
|
| 163 |
-
"@firebase/firestore": ["@firebase/firestore@4.17.1", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "@firebase/webchannel-wrapper": "1.0.7", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", "re2js": "^2.8.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-8lqPNf2w10CtYG+tayVjZO1pSyQpnhztQRudeD109VtXDzNbASTaYdO43sj5PMsDcWq0aOYY3RmJOUlXu9++jw=="],
|
| 164 |
-
|
| 165 |
-
"@firebase/firestore-compat": ["@firebase/firestore-compat@0.4.13", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/firestore": "4.17.1", "@firebase/firestore-types": "3.0.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-l9dCewxMzzLOIhcwTjERCKxrWOn1kZ9JvwOQq9zZNq4I/Nbvb/B9njT230V61uvQ9qZ3/qpUG758D8DDTEFXnw=="],
|
| 166 |
-
|
| 167 |
-
"@firebase/firestore-types": ["@firebase/firestore-types@3.0.5", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-dbdMAQkMd5dwWc48eupz/Y6/E9ruat3+gY5lhVKscvvT/HnDBEEMzJW38zdKhgdnglZHGk2vUsJMOHAphMHGMA=="],
|
| 168 |
-
|
| 169 |
-
"@firebase/functions": ["@firebase/functions@0.14.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.5", "@firebase/auth-interop-types": "0.2.6", "@firebase/component": "0.7.5", "@firebase/messaging-interop-types": "0.2.6", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-DhuYFr0eMhp+s/PNEk6SiMsYkc00+XVOUeKbrl8MOzQNZI3SKQpqoDR1d+keNDAutwmHRWTUAdXBoVPD+xxVUw=="],
|
| 170 |
-
|
| 171 |
-
"@firebase/functions-compat": ["@firebase/functions-compat@0.5.0", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/functions": "0.14.0", "@firebase/functions-types": "0.6.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-T3BDIToESZUHt7438wyKYMkRjKg3m1xAIux5fVpNvTRQlDOFLukWniQWBDhJ2znpU9kxMTR6+e31TVo8P15PZw=="],
|
| 172 |
-
|
| 173 |
-
"@firebase/functions-types": ["@firebase/functions-types@0.6.5", "", {}, "sha512-Zc0pURjthHXzSj54ZivCkzKDSV1r/wIpnmHdhq82q2yFFPVoncG/ZJjnVMiANvQfeww/QnElhzODpfwUucVwdA=="],
|
| 174 |
-
|
| 175 |
-
"@firebase/installations": ["@firebase/installations@0.6.24", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/util": "1.15.3", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-Ui52ey8wHoWqkBbXRKJEKYWylI0JZogZmoLS+o8Anh1bxWtK27ZYjLla1UJAAdC5DCKLJ2qAp0VpJOjg8VOX1g=="],
|
| 176 |
-
|
| 177 |
-
"@firebase/installations-compat": ["@firebase/installations-compat@0.2.24", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/installations": "0.6.24", "@firebase/installations-types": "0.5.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-8M5nlcWwYt881x83COP2odq5vgf+NgwJh+RMd4LRSv8JI1pxwDfgrDJOERrjTgfC9J5Z0vnQX4b1pdN914e0Zw=="],
|
| 178 |
-
|
| 179 |
-
"@firebase/installations-types": ["@firebase/installations-types@0.5.5", "", { "peerDependencies": { "@firebase/app-types": "0.x" } }, "sha512-e9UYcju3puDl1vdrcKIi5dExzHLameOT/Tc61Q48PYwxtsM1NzZh/ikGbdBQTsbRgg0EMZqdPr0/m5ODUBobrg=="],
|
| 180 |
-
|
| 181 |
-
"@firebase/logger": ["@firebase/logger@0.5.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg=="],
|
| 182 |
-
|
| 183 |
-
"@firebase/messaging": ["@firebase/messaging@0.13.2", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/installations": "0.6.24", "@firebase/messaging-interop-types": "0.2.6", "@firebase/util": "1.15.3", "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-KcZoqUu2ih4sLH91dW9tmyHjCR0IQyNzSLZunX5uq7cLeImXb4uO2I0wq5FACduO2I+FoTeWa2BtUv7Jpowqdw=="],
|
| 184 |
-
|
| 185 |
-
"@firebase/messaging-compat": ["@firebase/messaging-compat@0.2.29", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/messaging": "0.13.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-8Twe4CeYvAx8AzjBxyyQFKzinaMGGt13hPQBqaARQ2QZjagrEaqSVBe+Zy6F4L/vT8DV168yRgRu7W/RK7p+4w=="],
|
| 186 |
-
|
| 187 |
-
"@firebase/messaging-interop-types": ["@firebase/messaging-interop-types@0.2.6", "", {}, "sha512-MVzvkKe2V4H2dHu5oOxRfeKQcfTwWmCgnzsC4V1q3ixun5iiL8riCZ2qI35rDhCL4glPGiu0jHxDbpVNuTKfow=="],
|
| 188 |
-
|
| 189 |
-
"@firebase/performance": ["@firebase/performance@0.7.14", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/installations": "0.6.24", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0", "web-vitals": "^4.2.4" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-9PH1XEZVHErxGdbXluvGz4Uyjw4W955H8v7Mv9rHIybRrg5F2Ac2tvf5+mSf5EtnxWNmxrD2WLnvMFaZP4sMrQ=="],
|
| 190 |
-
|
| 191 |
-
"@firebase/performance-compat": ["@firebase/performance-compat@0.2.27", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/performance": "0.7.14", "@firebase/performance-types": "0.2.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-O/ozTf/EbChN94Pk7bd1eUC0PAC36726AwsaiJyC0bZzSBWfLGSWASGPH5pebUWXQPJtGxIJYRkkbX5l0Qa+Hw=="],
|
| 192 |
-
|
| 193 |
-
"@firebase/performance-types": ["@firebase/performance-types@0.2.5", "", {}, "sha512-PRzOgB+/M+6AKlEkY8a9xy5Ff5SfZPIh4iShXhn2WAcL/euSbK7bdLqWysHduunNJhGFsXa22Ag3ixqL6rQtYg=="],
|
| 194 |
-
|
| 195 |
-
"@firebase/remote-config": ["@firebase/remote-config@0.9.2", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/installations": "0.6.24", "@firebase/logger": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-Rii93DkXjM+RE/ytHdYa7EIiQLJbdBr+W8EEiqd/vbLCOC+1FdkDuRiumJBp6t3yewHGbdqbpjB3fk3z0cZ+8g=="],
|
| 196 |
-
|
| 197 |
-
"@firebase/remote-config-compat": ["@firebase/remote-config-compat@0.2.29", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/logger": "0.5.2", "@firebase/remote-config": "0.9.2", "@firebase/remote-config-types": "0.5.2", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-mY7JtTISK6F4g4T0x3kVepr5cp0NXL7f5yjIUXXGaTrg4qUlFBWRbENaHaqZ78dwmNcLm24h/yPsOVRlDXEXhA=="],
|
| 198 |
-
|
| 199 |
-
"@firebase/remote-config-types": ["@firebase/remote-config-types@0.5.2", "", {}, "sha512-i8k1omVfoAnaT1ZPv2FFjxMZrATYsO/GnFgHEj5+7fAJLzi8wLnGGv1WK06oEAD6ltrWXSO1HzeNyajn/NjMWw=="],
|
| 200 |
-
|
| 201 |
-
"@firebase/storage": ["@firebase/storage@0.14.5", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, "sha512-r2tozN/BlEewLi70tJNUQPzWwbex9GM7NgXZsamHKQrjKEfcR0i4jGgHBKAKA4hbwiPE9eNMb3hcxWnDpeJZwg=="],
|
| 202 |
-
|
| 203 |
-
"@firebase/storage-compat": ["@firebase/storage-compat@0.4.5", "", { "dependencies": { "@firebase/component": "0.7.5", "@firebase/storage": "0.14.5", "@firebase/storage-types": "0.8.5", "@firebase/util": "1.15.3", "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-compat": "0.x" } }, "sha512-vO0tFPxXbKDKdlTu8tYT08S9t9ezUTJYEdLCULpWxWq2aq6zojwN1QmAB+1a50AI/g47GlWUJn1XPncm/PDZsw=="],
|
| 204 |
-
|
| 205 |
-
"@firebase/storage-types": ["@firebase/storage-types@0.8.5", "", { "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "sha512-GEDs5P+rNUfNcS+wxIdOLAHficife2YXtvTJnyi3ssrX11AAtBg+nDUdbYh8vuBzWehVQZPmztGUUnud4L+4yg=="],
|
| 206 |
-
|
| 207 |
-
"@firebase/util": ["@firebase/util@1.15.3", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA=="],
|
| 208 |
-
|
| 209 |
-
"@firebase/webchannel-wrapper": ["@firebase/webchannel-wrapper@1.0.7", "", {}, "sha512-phBFwieDLvkZGYN9CE9ZFNEIoBVksprzsnCzQejCmCHtgwCXReeuRpoEGN9C4EbhONztv8NRV1tau6Rb9pONwQ=="],
|
| 210 |
-
|
| 211 |
-
"@google/genai": ["@google/genai@2.21.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q=="],
|
| 212 |
-
|
| 213 |
-
"@grpc/grpc-js": ["@grpc/grpc-js@1.9.16", "", { "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" } }, "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ=="],
|
| 214 |
-
|
| 215 |
-
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
|
| 216 |
-
|
| 217 |
-
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
| 218 |
-
|
| 219 |
-
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
| 220 |
-
|
| 221 |
-
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
| 222 |
-
|
| 223 |
-
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
| 224 |
-
|
| 225 |
-
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
| 226 |
-
|
| 227 |
-
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
| 228 |
-
|
| 229 |
-
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
| 230 |
-
|
| 231 |
-
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
| 232 |
-
|
| 233 |
-
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
|
| 234 |
-
|
| 235 |
-
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
|
| 236 |
-
|
| 237 |
-
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
|
| 238 |
-
|
| 239 |
-
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
| 240 |
-
|
| 241 |
-
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
| 242 |
-
|
| 243 |
-
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
| 244 |
-
|
| 245 |
-
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
| 246 |
-
|
| 247 |
-
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
| 248 |
-
|
| 249 |
-
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="],
|
| 250 |
-
|
| 251 |
-
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="],
|
| 252 |
-
|
| 253 |
-
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="],
|
| 254 |
-
|
| 255 |
-
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="],
|
| 256 |
-
|
| 257 |
-
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="],
|
| 258 |
-
|
| 259 |
-
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="],
|
| 260 |
-
|
| 261 |
-
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="],
|
| 262 |
-
|
| 263 |
-
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="],
|
| 264 |
-
|
| 265 |
-
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="],
|
| 266 |
-
|
| 267 |
-
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="],
|
| 268 |
-
|
| 269 |
-
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="],
|
| 270 |
-
|
| 271 |
-
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="],
|
| 272 |
-
|
| 273 |
-
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="],
|
| 274 |
-
|
| 275 |
-
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="],
|
| 276 |
-
|
| 277 |
-
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="],
|
| 278 |
-
|
| 279 |
-
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="],
|
| 280 |
-
|
| 281 |
-
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="],
|
| 282 |
-
|
| 283 |
-
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="],
|
| 284 |
-
|
| 285 |
-
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="],
|
| 286 |
-
|
| 287 |
-
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="],
|
| 288 |
-
|
| 289 |
-
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="],
|
| 290 |
-
|
| 291 |
-
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="],
|
| 292 |
-
|
| 293 |
-
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="],
|
| 294 |
-
|
| 295 |
-
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="],
|
| 296 |
-
|
| 297 |
-
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="],
|
| 298 |
-
|
| 299 |
-
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
| 300 |
-
|
| 301 |
-
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
| 302 |
-
|
| 303 |
-
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
| 304 |
-
|
| 305 |
-
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
| 306 |
-
|
| 307 |
-
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
| 308 |
-
|
| 309 |
-
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
| 310 |
-
|
| 311 |
-
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
| 312 |
-
|
| 313 |
-
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
| 314 |
-
|
| 315 |
-
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
| 316 |
-
|
| 317 |
-
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
| 318 |
-
|
| 319 |
-
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
| 320 |
-
|
| 321 |
-
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
| 322 |
-
|
| 323 |
-
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
| 324 |
-
|
| 325 |
-
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
| 326 |
-
|
| 327 |
-
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
|
| 328 |
-
|
| 329 |
-
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
| 330 |
-
|
| 331 |
-
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
| 332 |
-
|
| 333 |
-
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
| 334 |
-
|
| 335 |
-
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
| 336 |
-
|
| 337 |
-
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
| 338 |
-
|
| 339 |
-
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
| 340 |
-
|
| 341 |
-
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
| 342 |
-
|
| 343 |
-
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
|
| 344 |
-
|
| 345 |
-
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="],
|
| 346 |
-
|
| 347 |
-
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
| 348 |
-
|
| 349 |
-
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
| 350 |
-
|
| 351 |
-
"@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
|
| 352 |
-
|
| 353 |
-
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
| 354 |
-
|
| 355 |
-
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
| 356 |
-
|
| 357 |
-
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
| 358 |
-
|
| 359 |
-
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
| 360 |
-
|
| 361 |
-
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
| 362 |
-
|
| 363 |
-
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "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" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
| 364 |
-
|
| 365 |
-
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
| 366 |
-
|
| 367 |
-
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
| 368 |
-
|
| 369 |
-
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
| 370 |
-
|
| 371 |
-
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
| 372 |
-
|
| 373 |
-
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
| 374 |
-
|
| 375 |
-
"autoprefixer": ["autoprefixer@10.5.5", "", { "dependencies": { "browserslist": "^4.28.9", "caniuse-lite": "^1.0.30001810", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uiRYvQYe/nNSzBJ7OUnd2/TZVsAdob3blml44teEpee9Cc1f4rGZFewO+JT3Wo8mgFOSzNqes4FHZn/Qz8WOuw=="],
|
| 376 |
-
|
| 377 |
-
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
| 378 |
-
|
| 379 |
-
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="],
|
| 380 |
-
|
| 381 |
-
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
| 382 |
-
|
| 383 |
-
"body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="],
|
| 384 |
-
|
| 385 |
-
"browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="],
|
| 386 |
-
|
| 387 |
-
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
| 388 |
-
|
| 389 |
-
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
| 390 |
-
|
| 391 |
-
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
| 392 |
-
|
| 393 |
-
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
| 394 |
-
|
| 395 |
-
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
| 396 |
-
|
| 397 |
-
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
| 398 |
-
|
| 399 |
-
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
| 400 |
-
|
| 401 |
-
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
| 402 |
-
|
| 403 |
-
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
| 404 |
-
|
| 405 |
-
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
| 406 |
-
|
| 407 |
-
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
| 408 |
-
|
| 409 |
-
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
| 410 |
-
|
| 411 |
-
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
| 412 |
-
|
| 413 |
-
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
| 414 |
-
|
| 415 |
-
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
| 416 |
-
|
| 417 |
-
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
| 418 |
-
|
| 419 |
-
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
| 420 |
-
|
| 421 |
-
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
| 422 |
-
|
| 423 |
-
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
| 424 |
-
|
| 425 |
-
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
| 426 |
-
|
| 427 |
-
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
| 428 |
-
|
| 429 |
-
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
| 430 |
-
|
| 431 |
-
"electron-to-chromium": ["electron-to-chromium@1.5.422", "", {}, "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA=="],
|
| 432 |
-
|
| 433 |
-
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
| 434 |
-
|
| 435 |
-
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
| 436 |
-
|
| 437 |
-
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
| 438 |
-
|
| 439 |
-
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
| 440 |
-
|
| 441 |
-
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
| 442 |
-
|
| 443 |
-
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
| 444 |
-
|
| 445 |
-
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
| 446 |
-
|
| 447 |
-
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
| 448 |
-
|
| 449 |
-
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
| 450 |
-
|
| 451 |
-
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
| 452 |
-
|
| 453 |
-
"express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
|
| 454 |
-
|
| 455 |
-
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
| 456 |
-
|
| 457 |
-
"faye-websocket": ["faye-websocket@0.11.4", "", { "dependencies": { "websocket-driver": ">=0.5.1" } }, "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="],
|
| 458 |
-
|
| 459 |
-
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
| 460 |
-
|
| 461 |
-
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
| 462 |
-
|
| 463 |
-
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
| 464 |
-
|
| 465 |
-
"firebase": ["firebase@12.18.0", "", { "dependencies": { "@firebase/ai": "2.15.0", "@firebase/analytics": "0.10.24", "@firebase/analytics-compat": "0.2.30", "@firebase/app": "0.16.1", "@firebase/app-check": "0.13.1", "@firebase/app-check-compat": "0.4.7", "@firebase/app-compat": "0.5.17", "@firebase/app-types": "0.9.6", "@firebase/auth": "1.13.5", "@firebase/auth-compat": "0.6.10", "@firebase/data-connect": "0.7.4", "@firebase/database": "1.1.5", "@firebase/database-compat": "2.1.7", "@firebase/firestore": "4.17.1", "@firebase/firestore-compat": "0.4.13", "@firebase/functions": "0.14.0", "@firebase/functions-compat": "0.5.0", "@firebase/installations": "0.6.24", "@firebase/installations-compat": "0.2.24", "@firebase/messaging": "0.13.2", "@firebase/messaging-compat": "0.2.29", "@firebase/performance": "0.7.14", "@firebase/performance-compat": "0.2.27", "@firebase/remote-config": "0.9.2", "@firebase/remote-config-compat": "0.2.29", "@firebase/storage": "0.14.5", "@firebase/storage-compat": "0.4.5", "@firebase/util": "1.15.3" } }, "sha512-XaL6tlE5Xd20ZDhckqOMIw+JJTET+wTdeZPxQ7ihc42oxRb7kWUyn/j1LO5V9dH1xq8Rv5R71Pv1fBCdIkt9Rw=="],
|
| 466 |
-
|
| 467 |
-
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
| 468 |
-
|
| 469 |
-
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
| 470 |
-
|
| 471 |
-
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
| 472 |
-
|
| 473 |
-
"framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="],
|
| 474 |
-
|
| 475 |
-
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
| 476 |
-
|
| 477 |
-
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
| 478 |
-
|
| 479 |
-
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
| 480 |
-
|
| 481 |
-
"gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
|
| 482 |
-
|
| 483 |
-
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
| 484 |
-
|
| 485 |
-
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
| 486 |
-
|
| 487 |
-
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
| 488 |
-
|
| 489 |
-
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
| 490 |
-
|
| 491 |
-
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
| 492 |
-
|
| 493 |
-
"google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="],
|
| 494 |
-
|
| 495 |
-
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
| 496 |
-
|
| 497 |
-
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
| 498 |
-
|
| 499 |
-
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
| 500 |
-
|
| 501 |
-
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
| 502 |
-
|
| 503 |
-
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
| 504 |
-
|
| 505 |
-
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
| 506 |
-
|
| 507 |
-
"http-parser-js": ["http-parser-js@0.5.10", "", {}, "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="],
|
| 508 |
-
|
| 509 |
-
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
| 510 |
-
|
| 511 |
-
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
| 512 |
-
|
| 513 |
-
"idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
|
| 514 |
-
|
| 515 |
-
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
| 516 |
-
|
| 517 |
-
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
| 518 |
-
|
| 519 |
-
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
| 520 |
-
|
| 521 |
-
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
| 522 |
-
|
| 523 |
-
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
| 524 |
-
|
| 525 |
-
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
| 526 |
-
|
| 527 |
-
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
| 528 |
-
|
| 529 |
-
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
| 530 |
-
|
| 531 |
-
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
| 532 |
-
|
| 533 |
-
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
| 534 |
-
|
| 535 |
-
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
| 536 |
-
|
| 537 |
-
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
| 538 |
-
|
| 539 |
-
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
| 540 |
-
|
| 541 |
-
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
| 542 |
-
|
| 543 |
-
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
| 544 |
-
|
| 545 |
-
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
| 546 |
-
|
| 547 |
-
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
| 548 |
-
|
| 549 |
-
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
| 550 |
-
|
| 551 |
-
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
| 552 |
-
|
| 553 |
-
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
| 554 |
-
|
| 555 |
-
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
| 556 |
-
|
| 557 |
-
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
| 558 |
-
|
| 559 |
-
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
| 560 |
-
|
| 561 |
-
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
| 562 |
-
|
| 563 |
-
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
| 564 |
-
|
| 565 |
-
"lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
|
| 566 |
-
|
| 567 |
-
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
| 568 |
-
|
| 569 |
-
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
| 570 |
-
|
| 571 |
-
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
| 572 |
-
|
| 573 |
-
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
| 574 |
-
|
| 575 |
-
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
| 576 |
-
|
| 577 |
-
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
| 578 |
-
|
| 579 |
-
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
| 580 |
-
|
| 581 |
-
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
| 582 |
-
|
| 583 |
-
"motion": ["motion@12.43.0", "", { "dependencies": { "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ=="],
|
| 584 |
-
|
| 585 |
-
"motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
|
| 586 |
-
|
| 587 |
-
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
| 588 |
-
|
| 589 |
-
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
| 590 |
-
|
| 591 |
-
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
| 592 |
-
|
| 593 |
-
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
| 594 |
-
|
| 595 |
-
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
| 596 |
-
|
| 597 |
-
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
| 598 |
-
|
| 599 |
-
"node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="],
|
| 600 |
-
|
| 601 |
-
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
| 602 |
-
|
| 603 |
-
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
| 604 |
-
|
| 605 |
-
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
| 606 |
-
|
| 607 |
-
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
| 608 |
-
|
| 609 |
-
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
| 610 |
-
|
| 611 |
-
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
| 612 |
-
|
| 613 |
-
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
| 614 |
-
|
| 615 |
-
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
| 616 |
-
|
| 617 |
-
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
| 618 |
-
|
| 619 |
-
"protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="],
|
| 620 |
-
|
| 621 |
-
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
| 622 |
-
|
| 623 |
-
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
|
| 624 |
-
|
| 625 |
-
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
| 626 |
-
|
| 627 |
-
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
| 628 |
-
|
| 629 |
-
"re2js": ["re2js@2.8.6", "", {}, "sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg=="],
|
| 630 |
-
|
| 631 |
-
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
| 632 |
-
|
| 633 |
-
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
| 634 |
-
|
| 635 |
-
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
| 636 |
-
|
| 637 |
-
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
| 638 |
-
|
| 639 |
-
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
| 640 |
-
|
| 641 |
-
"rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="],
|
| 642 |
-
|
| 643 |
-
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
| 644 |
-
|
| 645 |
-
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
| 646 |
-
|
| 647 |
-
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
| 648 |
-
|
| 649 |
-
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
| 650 |
-
|
| 651 |
-
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
| 652 |
-
|
| 653 |
-
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
| 654 |
-
|
| 655 |
-
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
| 656 |
-
|
| 657 |
-
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
| 658 |
-
|
| 659 |
-
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
| 660 |
-
|
| 661 |
-
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
| 662 |
-
|
| 663 |
-
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
| 664 |
-
|
| 665 |
-
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
| 666 |
-
|
| 667 |
-
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
| 668 |
-
|
| 669 |
-
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
| 670 |
-
|
| 671 |
-
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
| 672 |
-
|
| 673 |
-
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
| 674 |
-
|
| 675 |
-
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
| 676 |
-
|
| 677 |
-
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
| 678 |
-
|
| 679 |
-
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
| 680 |
-
|
| 681 |
-
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
| 682 |
-
|
| 683 |
-
"tsx": ["tsx@4.23.13", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw=="],
|
| 684 |
-
|
| 685 |
-
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
| 686 |
-
|
| 687 |
-
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
| 688 |
-
|
| 689 |
-
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
| 690 |
-
|
| 691 |
-
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
| 692 |
-
|
| 693 |
-
"update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="],
|
| 694 |
-
|
| 695 |
-
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
| 696 |
-
|
| 697 |
-
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
| 698 |
-
|
| 699 |
-
"vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="],
|
| 700 |
-
|
| 701 |
-
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
| 702 |
-
|
| 703 |
-
"web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="],
|
| 704 |
-
|
| 705 |
-
"websocket-driver": ["websocket-driver@0.7.5", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA=="],
|
| 706 |
-
|
| 707 |
-
"websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="],
|
| 708 |
-
|
| 709 |
-
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
| 710 |
-
|
| 711 |
-
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
| 712 |
-
|
| 713 |
-
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
| 714 |
-
|
| 715 |
-
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
| 716 |
-
|
| 717 |
-
"yargs": ["yargs@17.7.3", "", { "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" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
|
| 718 |
-
|
| 719 |
-
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
| 720 |
-
|
| 721 |
-
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
| 722 |
-
|
| 723 |
-
"@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
| 724 |
-
|
| 725 |
-
"@grpc/grpc-js/@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
| 726 |
-
|
| 727 |
-
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
| 728 |
-
|
| 729 |
-
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
| 730 |
-
|
| 731 |
-
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
| 732 |
-
|
| 733 |
-
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
| 734 |
-
|
| 735 |
-
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
| 736 |
-
|
| 737 |
-
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
| 738 |
-
|
| 739 |
-
"@types/body-parser/@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
| 740 |
-
|
| 741 |
-
"@types/connect/@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
| 742 |
-
|
| 743 |
-
"@types/express-serve-static-core/@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
| 744 |
-
|
| 745 |
-
"@types/send/@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
| 746 |
-
|
| 747 |
-
"@types/serve-static/@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
| 748 |
-
|
| 749 |
-
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
| 750 |
-
|
| 751 |
-
"https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
| 752 |
-
|
| 753 |
-
"protobufjs/@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
|
| 754 |
-
|
| 755 |
-
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
| 756 |
-
|
| 757 |
-
"tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
| 758 |
-
|
| 759 |
-
"@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
| 760 |
-
|
| 761 |
-
"@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
| 762 |
-
|
| 763 |
-
"@grpc/grpc-js/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
| 764 |
-
|
| 765 |
-
"@types/body-parser/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
| 766 |
-
|
| 767 |
-
"@types/connect/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
| 768 |
-
|
| 769 |
-
"@types/express-serve-static-core/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
| 770 |
-
|
| 771 |
-
"@types/send/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
| 772 |
-
|
| 773 |
-
"@types/serve-static/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
| 774 |
-
|
| 775 |
-
"https-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
| 776 |
-
|
| 777 |
-
"protobufjs/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
| 778 |
-
|
| 779 |
-
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
| 780 |
-
|
| 781 |
-
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
| 782 |
-
|
| 783 |
-
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
| 784 |
-
|
| 785 |
-
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
| 786 |
-
|
| 787 |
-
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
| 788 |
-
|
| 789 |
-
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
| 790 |
-
|
| 791 |
-
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
| 792 |
-
|
| 793 |
-
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
| 794 |
-
|
| 795 |
-
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
| 796 |
-
|
| 797 |
-
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
| 798 |
-
|
| 799 |
-
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
| 800 |
-
|
| 801 |
-
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
| 802 |
-
|
| 803 |
-
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
| 804 |
-
|
| 805 |
-
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
| 806 |
-
|
| 807 |
-
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
| 808 |
-
|
| 809 |
-
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
| 810 |
-
|
| 811 |
-
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
| 812 |
-
|
| 813 |
-
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
| 814 |
-
|
| 815 |
-
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
| 816 |
-
|
| 817 |
-
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
| 818 |
-
|
| 819 |
-
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
| 820 |
-
|
| 821 |
-
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
| 822 |
-
|
| 823 |
-
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
| 824 |
-
|
| 825 |
-
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
| 826 |
-
|
| 827 |
-
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
| 828 |
-
|
| 829 |
-
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
| 830 |
-
}
|
| 831 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
firebase-applet-config.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"projectId": "gen-lang-client-0212219650",
|
| 3 |
-
"appId": "1:580282245939:web:f6211064861f0e63241446",
|
| 4 |
-
"apiKey": "AIzaSyC2RYChsvJQ36tNTut-cPzxgDvb-FYFwiI",
|
| 5 |
-
"authDomain": "gen-lang-client-0212219650.firebaseapp.com",
|
| 6 |
-
"storageBucket": "gen-lang-client-0212219650.firebasestorage.app",
|
| 7 |
-
"messagingSenderId": "580282245939",
|
| 8 |
-
"measurementId": "",
|
| 9 |
-
"oAuthClientId": "580282245939-1mo82qkdoo06vs5u5ogsc9j9lvqqlsfn.apps.googleusercontent.com",
|
| 10 |
-
"recaptchaSiteKey": ""
|
| 11 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
| 9 |
<meta property="og:description" content="Terminal quantitativo híbrido multi-timeframe com confluência de 4 escolas de trading, IA institucional, backtesting diário e alertas críticos." />
|
| 10 |
<meta property="og:type" content="website" />
|
| 11 |
<meta name="twitter:card" content="summary_large_image" />
|
| 12 |
-
<script type="module" crossorigin src="./assets/index-
|
| 13 |
<link rel="stylesheet" crossorigin href="./assets/index-CNuvSQCK.css">
|
| 14 |
</head>
|
| 15 |
<body>
|
|
|
|
| 9 |
<meta property="og:description" content="Terminal quantitativo híbrido multi-timeframe com confluência de 4 escolas de trading, IA institucional, backtesting diário e alertas críticos." />
|
| 10 |
<meta property="og:type" content="website" />
|
| 11 |
<meta name="twitter:card" content="summary_large_image" />
|
| 12 |
+
<script type="module" crossorigin src="./assets/index-Bh0tM6mi.js"></script>
|
| 13 |
<link rel="stylesheet" crossorigin href="./assets/index-CNuvSQCK.css">
|
| 14 |
</head>
|
| 15 |
<body>
|
metadata.json
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "The God Protocol v2026",
|
| 3 |
-
"description": "Terminal quantitativo híbrido multi-timeframe com confluência de 4 escolas de trading, IA institucional, backtesting diário e alertas críticos.",
|
| 4 |
-
"requestFramePermissions": [],
|
| 5 |
-
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
| 6 |
-
}
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package.json
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "react-example",
|
| 3 |
-
"private": true,
|
| 4 |
-
"version": "0.0.0",
|
| 5 |
-
"type": "module",
|
| 6 |
-
"scripts": {
|
| 7 |
-
"dev": "tsx server.ts",
|
| 8 |
-
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
|
| 9 |
-
"start": "node dist/server.cjs",
|
| 10 |
-
"clean": "rm -rf dist server.js",
|
| 11 |
-
"lint": "tsc --noEmit"
|
| 12 |
-
},
|
| 13 |
-
"dependencies": {
|
| 14 |
-
"@google/genai": "^2.4.0",
|
| 15 |
-
"@tailwindcss/vite": "^4.1.14",
|
| 16 |
-
"@vitejs/plugin-react": "^5.0.4",
|
| 17 |
-
"dotenv": "^17.2.3",
|
| 18 |
-
"express": "^4.21.2",
|
| 19 |
-
"firebase": "^12.18.0",
|
| 20 |
-
"lucide-react": "^0.546.0",
|
| 21 |
-
"motion": "^12.23.24",
|
| 22 |
-
"react": "^19.0.1",
|
| 23 |
-
"react-dom": "^19.0.1",
|
| 24 |
-
"vite": "^6.2.3"
|
| 25 |
-
},
|
| 26 |
-
"devDependencies": {
|
| 27 |
-
"@types/node": "^22.14.0",
|
| 28 |
-
"autoprefixer": "^10.4.21",
|
| 29 |
-
"esbuild": "^0.25.0",
|
| 30 |
-
"tailwindcss": "^4.1.14",
|
| 31 |
-
"tsx": "^4.21.0",
|
| 32 |
-
"typescript": "~5.8.2",
|
| 33 |
-
"vite": "^6.2.3",
|
| 34 |
-
"@types/express": "^4.17.21"
|
| 35 |
-
}
|
| 36 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server.cjs
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
server.cjs.map
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
server.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
import express from 'express';
|
| 2 |
-
import path from 'path';
|
| 3 |
-
import { fileURLToPath } from 'url';
|
| 4 |
-
import { createServer as createViteServer } from 'vite';
|
| 5 |
-
import { GoogleGenAI } from '@google/genai';
|
| 6 |
-
import dotenv from 'dotenv';
|
| 7 |
-
import { TradeSignal, SystemNotification } from './src/types.js';
|
| 8 |
-
import { fetchTop100Cryptos, syncRealTimePrices, getBrasiliaTimeStr } from './server/realMarketData.js';
|
| 9 |
-
import { getMarketSnapshot } from './server/market/marketSnapshotService.js';
|
| 10 |
-
import { analyzeMarket } from './server/confluence/marketAnalysisService.js';
|
| 11 |
-
import { fetchHistoricalBinanceCandles } from './server/backtest/historicalDataService.js';
|
| 12 |
-
import { fetchHistoricalBinanceFunding } from './server/derivatives/historicalFundingService.js';
|
| 13 |
-
import { runHistoricalBacktest } from './server/backtest/historicalBacktest.js';
|
| 14 |
-
import { generateTradeSignal } from './server/signal/signalEngine.js';
|
| 15 |
-
import { fetchRealCandles } from './server/market/candleService.js';
|
| 16 |
-
import { buildQuantAnalystPrompt, parseQuantAnalystResponse } from './server/ai/quantAnalyst.js';
|
| 17 |
-
import { paperTradingLoop } from './server/paper/paperTradingLoop.js';
|
| 18 |
-
import { buildQuantitativeLab } from './server/quant/quantitativeLab.js';
|
| 19 |
-
import { validateSelectedParameters } from './server/quant/parameterValidation.js';
|
| 20 |
-
|
| 21 |
-
dotenv.config();
|
| 22 |
-
const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = 3000; app.use(express.json());
|
| 23 |
-
let genAI: GoogleGenAI | null = null;
|
| 24 |
-
if (process.env.GEMINI_API_KEY) { try { genAI = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY, httpOptions: { headers: { 'User-Agent': 'aistudio-build' } } }); } catch (err) { console.error('Error initializing GoogleGenAI:', err); } }
|
| 25 |
-
let cachedSignals: TradeSignal[] = []; let cachedNotifications: SystemNotification[] = []; let isRefreshing = false; let lastRefreshTime = 0; let lastSourceInfo = 'Iniciando varredura das 100 maiores criptos por Market Cap...';
|
| 26 |
-
async function refreshMarketData(): Promise<void> { if (isRefreshing) return; isRefreshing = true; try { const signals = await fetchTop100Cryptos(); if (signals?.length) { cachedSignals = signals; lastRefreshTime = Date.now(); lastSourceInfo = 'Top 100 Criptomoedas por Market Cap (Tempo Real • CoinGecko + Binance)'; } const validSignals = cachedSignals.filter(s => s.passedFilter); validSignals.forEach(signal => { if (!cachedNotifications.some(n => n.symbol === signal.symbol && Date.now() - n.timestamp < 900000)) { const timeBrasilia = getBrasiliaTimeStr(Date.now(), true); cachedNotifications.unshift({ id: `notif-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, timestamp: Date.now(), timeStr: timeBrasilia, type: 'CRITICAL_SIGNAL', severity: 'high', symbol: signal.symbol, title: `Setup Quantitativo Confirmado: ${signal.symbol} (#${signal.marketCapRank || 0})`, message: `Confluência institucional atingiu ${signal.confidence}%. Entrada em $${signal.entryPrice} e SL dinâmico em $${signal.stopLoss} (R/R 1:${signal.riskReward}) • ${timeBrasilia} (Horário de Brasília).`, read: false, actionable: true }); } }); cachedSignals.forEach(signal => { if (Math.abs(signal.change24h) >= 7 && !cachedNotifications.some(n => n.symbol === signal.symbol && n.type === 'FUNDING_ALERT' && Date.now() - n.timestamp < 1200000)) { const timeBrasilia = getBrasiliaTimeStr(Date.now(), true); cachedNotifications.unshift({ id: `notif-vol-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, timestamp: Date.now(), timeStr: timeBrasilia, type: 'FUNDING_ALERT', severity: 'medium', symbol: signal.symbol, title: `Alerta de Volatilidade: ${signal.symbol} (#${signal.marketCapRank || 0})`, message: `Variação de 24h atingiu ${signal.change24h > 0 ? '+' : ''}${signal.change24h}% no ativo #${signal.marketCapRank || 0} • ${timeBrasilia} (Brasília).`, read: false }); } }); if (cachedNotifications.length > 50) cachedNotifications = cachedNotifications.slice(0, 50); } catch (err: any) { console.error('Failed to refresh real-time market data:', err.message); } finally { isRefreshing = false; } }
|
| 27 |
-
refreshMarketData(); setInterval(refreshMarketData, 35000); setInterval(async () => { if (cachedSignals.length > 0) { await syncRealTimePrices(); lastRefreshTime = Date.now(); } }, 6000);
|
| 28 |
-
|
| 29 |
-
app.get('/api/market/candles/:symbol', async (req, res) => { try { const symbol = decodeURIComponent(req.params.symbol).toUpperCase(); if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ error: 'Símbolo inválido. Use o formato BTC/USDT.' }); const snapshot = await getMarketSnapshot(symbol, { forceRefresh: req.query.refresh === 'true' }); return res.json({ success: true, source: 'Binance/OKX real OHLCV', snapshot }); } catch (error: any) { console.error('Real market snapshot error:', error); return res.status(502).json({ success: false, error: error?.message || 'Falha ao carregar dados reais de mercado.' }); } });
|
| 30 |
-
app.get('/api/market/analysis/:symbol', async (req, res) => { try { const symbol = decodeURIComponent(req.params.symbol).toUpperCase(); if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ error: 'Símbolo inválido. Use o formato BTC/USDT.' }); const analysis = await analyzeMarket(symbol, req.query.refresh === 'true'); return res.json({ success: true, source: 'Binance/OKX real OHLCV + indicadores + estrutura + SMC + divergências + Gann + Wyckoff', analysis }); } catch (error: any) { console.error('Quantitative market analysis error:', error); return res.status(502).json({ success: false, error: error?.message || 'Falha ao executar análise quantitativa.' }); } });
|
| 31 |
-
app.get('/api/signal/:symbol', async (req, res) => { try { const symbol = decodeURIComponent(req.params.symbol || 'BTC/USDT').toUpperCase().replace('-', '/'); if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: 'Símbolo inválido. Use BTC/USDT.' }); const riskPercent = Number(req.query.riskPercent ?? 1); const analysis = await analyzeMarket(symbol, req.query.refresh === 'true'); const series = await fetchRealCandles(symbol, '15m', 500); const signal = generateTradeSignal(analysis, series.candles, Number.isFinite(riskPercent) ? riskPercent : 1); return res.json({ success: true, source: 'Binance/OKX real OHLCV + quantitative signal engine', signal, generatedAt: Date.now() }); } catch (error: any) { console.error('Signal engine error:', error); return res.status(502).json({ success: false, error: error?.message || 'Falha ao gerar sinal.' }); } });
|
| 32 |
-
app.get('/api/paper/status', (_req, res) => res.json({ success: true, ...paperTradingLoop.getStatus(), state: paperTradingLoop.getEngine().getState() })); app.post('/api/paper/start', (_req, res) => { paperTradingLoop.start(); res.json({ success: true, ...paperTradingLoop.getStatus() }); }); app.post('/api/paper/stop', (_req, res) => { paperTradingLoop.stop(); res.json({ success: true, ...paperTradingLoop.getStatus() }); }); app.post('/api/paper/tick', async (_req, res) => { await paperTradingLoop.tick(); const status = paperTradingLoop.getStatus(); const success = status.lastError === null; return res.status(success ? 200 : 502).json({ success, ...status, state: paperTradingLoop.getEngine().getState() }); }); app.post('/api/paper/reset', (_req, res) => { paperTradingLoop.stop(); const state = paperTradingLoop.getEngine().reset(); return res.json({ success: true, ...paperTradingLoop.getStatus(), state }); }); app.post('/api/paper/close/:positionId', (req, res) => { const price = Number(req.body?.price); const closed = paperTradingLoop.getEngine().close(req.params.positionId, price, 'MANUAL'); if (!closed) return res.status(404).json({ success: false, error: 'Posição não encontrada ou preço inválido.' }); return res.json({ success: true, position: closed, state: paperTradingLoop.getEngine().getState() }); });
|
| 33 |
-
app.get('/api/market/signals', async (_req, res) => { if (!cachedSignals.length) await refreshMarketData(); res.json({ timestamp: lastRefreshTime || Date.now(), version: 'The God Protocol v2026 (v4.0)', dataSource: lastSourceInfo, isRealTime: true, signals: cachedSignals, activeCount: cachedSignals.filter(s => s.passedFilter).length, monitoredCount: cachedSignals.length }); }); app.post('/api/market/refresh-prices', async (_req, res) => { const updatedSignals = await syncRealTimePrices(); lastRefreshTime = Date.now(); res.json({ success: true, timestamp: lastRefreshTime, signals: updatedSignals, activeCount: updatedSignals.filter(s => s.passedFilter).length }); }); app.post('/api/market/scan', async (_req, res) => { await refreshMarketData(); res.json({ success: true, message: 'Varredura quantitativa em tempo real via Binance.US + OKX concluída com sucesso.', dataSource: lastSourceInfo, signals: cachedSignals }); });
|
| 34 |
-
app.post('/api/ai-analysis', async (req, res) => { try { const requestedSymbol = typeof req.body?.symbol === 'string' ? req.body.symbol.toUpperCase().replace('-', '/') : 'BTC/USDT'; if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(requestedSymbol)) return res.status(400).json({ error: 'Símbolo inválido. Use BTC/USDT.' }); const riskPercent = Number(req.body?.riskPercent ?? 1); const analysis = await analyzeMarket(requestedSymbol, true); const series = await fetchRealCandles(requestedSymbol, '15m', 500); const signal = generateTradeSignal(analysis, series.candles, Number.isFinite(riskPercent) ? riskPercent : 1); const model = 'gemini-3.8-flash'; if (!genAI) return res.json({ success: true, aiAvailable: false, analysis: { decision: 'WEAKEN', rationale: 'GEMINI_API_KEY não configurada; análise determinística disponível.', riskFlags: ['gemini_api_unavailable'], model: 'local-fallback' }, signal }); const prompt = buildQuantAnalystPrompt({ analysis, signal }); const response = await genAI.models.generateContent({ model, contents: prompt }); const quantAnalyst = parseQuantAnalystResponse(response.text || '', model); return res.json({ success: true, aiAvailable: true, analysis: quantAnalyst, signal, generatedAt: Date.now() }); } catch (error: any) { console.error('Gemini Quant Analyst error:', error); return res.status(502).json({ success: false, error: error?.message || 'Falha no Quant Analyst.' }); } });
|
| 35 |
-
app.get('/api/backtest', async (req, res) => { try { const symbol = String(req.query.symbol || 'BTC/USDT').toUpperCase(); if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: 'Símbolo inválido. Use o formato BTC/USDT.' }); const requestedDays = Number.parseInt(String(req.query.days || '60'), 10); const days = Number.isFinite(requestedDays) ? Math.max(7, Math.min(requestedDays, 90)) : 60; const endTime = Date.now(); const startTime = endTime - days * 86_400_000; const [candles, historicalFunding] = await Promise.all([fetchHistoricalBinanceCandles(symbol, '15m', startTime, endTime), fetchHistoricalBinanceFunding(symbol, startTime, endTime)]); if (candles.length < 300) return res.status(422).json({ success: false, error: `Histórico insuficiente: ${candles.length} candles.` }); const result = runHistoricalBacktest({ symbol, candles, initialCapital: 10_000, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220, historicalFunding }); return res.json({ success: true, source: 'Binance real OHLCV + Binance Futures historical funding', parameters: { symbol, timeframe: '15m', days, candles: candles.length, fundingEvents: historicalFunding.length, riskPerTradePercent: 1, rewardRisk: 2, maxHoldingBars: 32 }, ...result }); } catch (error: any) { console.error('Historical backtest error:', error); return res.status(502).json({ success: false, error: error?.message || 'Falha ao executar backtest histórico real.' }); } });
|
| 36 |
-
app.get('/api/quant/lab', async (req, res) => { try { const symbol = String(req.query.symbol || 'BTC/USDT').toUpperCase(); const requestedDays = Number.parseInt(String(req.query.days || '30'), 10); const days = Number.isFinite(requestedDays) ? Math.max(7, Math.min(requestedDays, 90)) : 30; if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: 'Símbolo inválido. Use o formato BTC/USDT.' }); const endTime = Date.now(); const startTime = endTime - days * 86_400_000; const [candles, historicalFunding] = await Promise.all([fetchHistoricalBinanceCandles(symbol, '15m', startTime, endTime), fetchHistoricalBinanceFunding(symbol, startTime, endTime)]); if (candles.length < 300) return res.status(422).json({ success: false, error: `Histórico insuficiente: ${candles.length} candles.` }); const options = { symbol, candles, initialCapital: 10_000, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220, historicalFunding } as const; const backtest = runHistoricalBacktest(options); const paperState = paperTradingLoop.getEngine().getState(); const lab = buildQuantitativeLab(backtest, paperState, candles); return res.json({ success: true, source: 'Binance real OHLCV + Binance Futures historical funding • Quant Lab completo', parameters: { symbol, timeframe: '15m', days, candles: candles.length, fundingEvents: historicalFunding.length, riskPerTradePercent: 1, rewardRisk: 2, maxHoldingBars: 32 }, ...lab }); } catch (error: any) { console.error('Quantitative lab error:', error); return res.status(502).json({ success: false, error: error?.message || 'Falha ao executar Quant Lab.' }); } });
|
| 37 |
-
app.get('/api/quant/validate', async (req, res) => { try { const symbol = String(req.query.symbol || 'BTC/USDT').toUpperCase(); const requestedDays = Number.parseInt(String(req.query.days || '180'), 10); const days = Number.isFinite(requestedDays) ? Math.max(90, Math.min(requestedDays, 180)) : 180; const trainPercent = Number(req.query.trainPercent ?? 70); if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: 'Símbolo inválido. Use o formato BTC/USDT.' }); const endTime = Date.now(); const startTime = endTime - days * 86_400_000; const [candles, historicalFunding] = await Promise.all([fetchHistoricalBinanceCandles(symbol, '15m', startTime, endTime), fetchHistoricalBinanceFunding(symbol, startTime, endTime)]); if (candles.length < 5000) return res.status(422).json({ success: false, error: `Histórico insuficiente para OOS multi-regime: ${candles.length} candles. Recomenda-se pelo menos 5.000.` }); const baseOptions = { initialCapital: 10_000, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220, historicalFunding }; const validation = validateSelectedParameters(symbol, candles, baseOptions, Number.isFinite(trainPercent) ? trainPercent : 70); return res.json({ success: true, source: 'Binance real OHLCV + Binance Futures historical funding • extended multi-regime OOS validation', parameters: { symbol, timeframe: '15m', days, candles: candles.length, fundingEvents: historicalFunding.length, trainPercent: validation.split.trainPercent, regimeWindowDays: days }, ...validation }); } catch (error: any) { console.error('Parameter OOS validation error:', error); return res.status(502).json({ success: false, error: error?.message || 'Falha na validação OOS multi-regime.' }); } });
|
| 38 |
-
app.get('/api/notifications', (_req, res) => res.json({ notifications: cachedNotifications, unreadCount: cachedNotifications.filter(n => !n.read).length })); app.post('/api/notifications/mark-read', (_req, res) => { cachedNotifications.forEach(n => { n.read = true; }); res.json({ success: true, unreadCount: 0 }); }); app.post('/api/notifications/clear', (_req, res) => { cachedNotifications = []; res.json({ success: true, count: 0 }); });
|
| 39 |
-
async function startServer() { if (process.env.NODE_ENV !== 'production') { const vite = await createViteServer({ server: { middlewareMode: true }, appType: 'spa' }); app.use(vite.middlewares); } else { const distPath = path.join(process.cwd(), 'dist'); app.use(express.static(distPath)); app.get('*', (_req, res) => res.sendFile(path.join(distPath, 'index.html'))); } app.listen(PORT, '0.0.0.0', () => console.log(`[The God Protocol v2026] Server running on http://0.0.0.0:${PORT}`)); }
|
| 40 |
-
startServer();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/ai/quantAnalyst.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
import type { MarketAnalysis } from '../confluence/marketAnalysisService.js';
|
| 2 |
-
import type { TradeSignal } from '../signal/signalEngine.js';
|
| 3 |
-
|
| 4 |
-
export type QuantAnalystDecision = 'CONFIRM' | 'WEAKEN' | 'REJECT';
|
| 5 |
-
|
| 6 |
-
export interface QuantAnalystInput { analysis: MarketAnalysis; signal: TradeSignal; }
|
| 7 |
-
export interface QuantAnalystResult { decision: QuantAnalystDecision; rationale: string; riskFlags: string[]; model: string; }
|
| 8 |
-
|
| 9 |
-
function compactAnalysis(analysis: MarketAnalysis, signal: TradeSignal): string {
|
| 10 |
-
const timeframes = analysis.timeframes.map(tf => ({
|
| 11 |
-
timeframe: tf.timeframe,
|
| 12 |
-
trend: tf.structure.trend,
|
| 13 |
-
rsi: tf.indicators.rsi14,
|
| 14 |
-
atr: tf.indicators.atr14,
|
| 15 |
-
structure: tf.structure.events.slice(-4),
|
| 16 |
-
smcBias: tf.smc.bias,
|
| 17 |
-
latestFvg: tf.smc.fairValueGaps.at(-1) ?? null,
|
| 18 |
-
latestOrderBlock: tf.smc.orderBlocks.at(-1) ?? null,
|
| 19 |
-
divergence: tf.divergences.latest,
|
| 20 |
-
gann: tf.gann,
|
| 21 |
-
wyckoff: tf.wyckoff,
|
| 22 |
-
}));
|
| 23 |
-
return JSON.stringify({ symbol: analysis.symbol, fetchedAt: analysis.fetchedAt, confluence: analysis.confluence, derivatives: analysis.derivatives, signal, timeframes });
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
export function buildQuantAnalystPrompt(input: QuantAnalystInput): string {
|
| 27 |
-
return `Você é o Quant Analyst de um sistema profissional de trading de criptomoedas.\n\nSua função é auditar um sinal quantitativo já calculado. NÃO invente preços, indicadores, níveis, eventos ou dados ausentes. NÃO substitua os cálculos determinísticos.\n\nRegras:\n1. Use somente os dados JSON fornecidos.\n2. Avalie alinhamento entre 15m, 1H, 4H e 1D.\n3. Dê peso especial à estrutura, SMC, Wyckoff, divergências e derivativos quando disponíveis.\n4. Procure conflitos, baixa qualidade, divergências contra a direção, ausência de confirmação e risco de contexto.\n5. A decisão deve ser exatamente uma de: CONFIRM, WEAKEN ou REJECT.\n6. CONFIRM apoia o sinal. WEAKEN indica conflitos relevantes. REJECT indica conflito estrutural ou dados insuficientes.\n7. Seja objetivo e não forneça promessa de lucro.\n\nResponda SOMENTE em JSON válido: {"decision":"CONFIRM|WEAKEN|REJECT","rationale":"...","riskFlags":["..."]}\n\nDADOS:\n${compactAnalysis(input.analysis, input.signal)}`;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
export function parseQuantAnalystResponse(text: string, model: string): QuantAnalystResult {
|
| 31 |
-
const fallback: QuantAnalystResult = { decision: 'WEAKEN', rationale: 'A resposta do modelo não pôde ser validada como JSON estruturado.', riskFlags: ['ai_response_invalid'], model };
|
| 32 |
-
try {
|
| 33 |
-
const parsed = JSON.parse(text) as Partial<QuantAnalystResult>;
|
| 34 |
-
if (!['CONFIRM', 'WEAKEN', 'REJECT'].includes(String(parsed.decision))) return fallback;
|
| 35 |
-
return {
|
| 36 |
-
decision: parsed.decision as QuantAnalystDecision,
|
| 37 |
-
rationale: typeof parsed.rationale === 'string' ? parsed.rationale : 'Sem justificativa estruturada.',
|
| 38 |
-
riskFlags: Array.isArray(parsed.riskFlags) ? parsed.riskFlags.filter((flag): flag is string => typeof flag === 'string').slice(0, 10) : [],
|
| 39 |
-
model,
|
| 40 |
-
};
|
| 41 |
-
} catch { return fallback; }
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/backtest/historicalBacktest.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import type { HistoricalFundingRate } from '../derivatives/historicalFundingService.js';
|
| 3 |
-
import { calculateIndicators } from '../indicators/technicalIndicators.js';
|
| 4 |
-
import { analyzeMarketStructure } from '../structure/marketStructure.js';
|
| 5 |
-
import { analyzeSMC } from '../structure/smc.js';
|
| 6 |
-
import { detectDivergences } from '../divergence/divergenceEngine.js';
|
| 7 |
-
import { analyzeGann } from '../gann/gannEngine.js';
|
| 8 |
-
import { analyzeWyckoff } from '../wyckoff/wyckoffEngine.js';
|
| 9 |
-
import { calculateConfluence } from '../confluence/confluenceEngine.js';
|
| 10 |
-
|
| 11 |
-
export interface HistoricalBacktestOptions {
|
| 12 |
-
symbol: string;
|
| 13 |
-
candles: Candle[];
|
| 14 |
-
initialCapital?: number;
|
| 15 |
-
riskPerTradePercent?: number;
|
| 16 |
-
minScore?: number;
|
| 17 |
-
minConfidence?: number;
|
| 18 |
-
atrStopMultiple?: number;
|
| 19 |
-
rewardRisk?: number;
|
| 20 |
-
maxHoldingBars?: number;
|
| 21 |
-
warmupBars?: number;
|
| 22 |
-
feeBpsPerSide?: number;
|
| 23 |
-
slippageBpsPerSide?: number;
|
| 24 |
-
latencySlippageBpsPerSide?: number;
|
| 25 |
-
fundingRatePer8h?: number;
|
| 26 |
-
historicalFunding?: HistoricalFundingRate[];
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export interface HistoricalBacktestTrade {
|
| 30 |
-
id: string; timestamp: number; date: string; symbol: string; direction: 'LONG' | 'SHORT'; entryPrice: number; exitPrice: number; stopLoss: number; takeProfit: number; rrRatio: number; confidence: number; score: number; grossPnlPercent: number; feePercent: number; slippagePercent: number; pnlPercent: number; pnlR: number; grossPnlR: number; feesR: number; fundingPercent: number; fundingR: number; status: 'TP ATINGIDO' | 'SL ATINGIDO' | 'TIMEOUT'; holdingBars: number;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
export interface HistoricalBacktestResult {
|
| 34 |
-
symbol: string; timeframe: string; startDate: string; endDate: string; periodDays: number; initialCapital: number; finalCapital: number; totalTrades: number; winningTrades: number; losingTrades: number; winRate: number; profitFactor: number; netProfitPercent: number; grossProfitPercent: number; totalFeesPercent: number; totalSlippagePercent: number; totalFundingPercent: number; maxDrawdownPercent: number; sharpeRatio: number; sortinoRatio: number; averageRR: number; expectancyR: number; grossExpectancyR: number; trades: HistoricalBacktestTrade[]; equityCurve: { date: string; equity: number; tradePnl: number; drawdown: number }[]; costModel: { feeBpsPerSide: number; slippageBpsPerSide: number; latencySlippageBpsPerSide: number; fundingRatePer8h: number; fundingIncluded: boolean; fundingSource: 'historical-binance' | 'proxy' | 'none' };
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
function mean(values: number[]): number { return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; }
|
| 38 |
-
function std(values: number[]): number { if (values.length < 2) return 0; const m = mean(values); return Math.sqrt(mean(values.map(v => (v - m) ** 2))); }
|
| 39 |
-
function calculateRatio(values: number[], downsideOnly = false): number { if (values.length < 2) return 0; const filtered = downsideOnly ? values.filter(v => v < 0) : values; const denominator = std(filtered); return denominator === 0 ? 0 : mean(values) / denominator * Math.sqrt(values.length); }
|
| 40 |
-
function clampIndex(index: number, length: number): number { return Math.max(0, Math.min(index, length - 1)); }
|
| 41 |
-
|
| 42 |
-
function historicalFundingSum(funding: HistoricalFundingRate[], entryTimestamp: number, exitTimestamp: number): number {
|
| 43 |
-
if (!funding.length) return 0;
|
| 44 |
-
let total = 0;
|
| 45 |
-
for (const row of funding) {
|
| 46 |
-
if (row.timestamp >= entryTimestamp && row.timestamp <= exitTimestamp) total += row.fundingRate;
|
| 47 |
-
}
|
| 48 |
-
return total;
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
/** Historical simulation uses only candles available at the signal timestamp. */
|
| 52 |
-
export function runHistoricalBacktest(options: HistoricalBacktestOptions): HistoricalBacktestResult {
|
| 53 |
-
const candles = [...options.candles].sort((a, b) => a.timestamp - b.timestamp);
|
| 54 |
-
const initialCapital = options.initialCapital ?? 10_000;
|
| 55 |
-
const riskPerTradePercent = options.riskPerTradePercent ?? 1;
|
| 56 |
-
const minScore = options.minScore ?? 35;
|
| 57 |
-
const minConfidence = options.minConfidence ?? 50;
|
| 58 |
-
const atrStopMultiple = options.atrStopMultiple ?? 1.5;
|
| 59 |
-
const rewardRisk = options.rewardRisk ?? 2;
|
| 60 |
-
const maxHoldingBars = options.maxHoldingBars ?? 32;
|
| 61 |
-
const warmupBars = Math.max(options.warmupBars ?? 220, 220);
|
| 62 |
-
const feeBpsPerSide = Math.max(0, options.feeBpsPerSide ?? 5);
|
| 63 |
-
const baseSlippageBpsPerSide = Math.max(0, options.slippageBpsPerSide ?? 2);
|
| 64 |
-
const latencySlippageBpsPerSide = Math.max(0, options.latencySlippageBpsPerSide ?? 1);
|
| 65 |
-
const effectiveSlippageBpsPerSide = baseSlippageBpsPerSide + latencySlippageBpsPerSide;
|
| 66 |
-
const proxyFundingRatePer8h = options.fundingRatePer8h ?? 0.0001;
|
| 67 |
-
const feeRate = feeBpsPerSide / 10_000;
|
| 68 |
-
const slippageRate = effectiveSlippageBpsPerSide / 10_000;
|
| 69 |
-
const fundingRate = Number.isFinite(proxyFundingRatePer8h) ? Math.max(-0.01, Math.min(0.01, proxyFundingRatePer8h)) : 0;
|
| 70 |
-
const historicalFunding = [...(options.historicalFunding ?? [])].filter(r => Number.isFinite(r.timestamp) && Number.isFinite(r.fundingRate)).sort((a, b) => a.timestamp - b.timestamp);
|
| 71 |
-
const hasHistoricalFunding = historicalFunding.length > 0;
|
| 72 |
-
|
| 73 |
-
const trades: HistoricalBacktestTrade[] = [];
|
| 74 |
-
const equityCurve: HistoricalBacktestResult['equityCurve'] = [];
|
| 75 |
-
let equity = initialCapital; let peak = equity; let maxDrawdownPercent = 0; let totalFees = 0; let totalSlippage = 0; let totalFunding = 0; let grossProfit = 0; let nextFreeIndex = warmupBars;
|
| 76 |
-
|
| 77 |
-
for (let i = warmupBars; i < candles.length - 2; i += 1) {
|
| 78 |
-
if (i < nextFreeIndex) continue;
|
| 79 |
-
const history = candles.slice(0, i + 1);
|
| 80 |
-
const indicators = calculateIndicators(history); const structure = analyzeMarketStructure(history); const smc = analyzeSMC(history, structure, indicators); const divergences = detectDivergences(history, indicators); const gann = analyzeGann(history, structure); const wyckoff = analyzeWyckoff(history, structure, indicators); const confluence = calculateConfluence([{ timeframe: '15m', indicators, structure, smc, divergences, gann, wyckoff }], null);
|
| 81 |
-
if (Math.abs(confluence.score) < minScore || confluence.confidence < minConfidence || confluence.entryQuality === 'avoid') continue;
|
| 82 |
-
const direction: 'LONG' | 'SHORT' = confluence.bias === 'bullish' ? 'LONG' : 'SHORT'; if (confluence.bias === 'neutral') continue;
|
| 83 |
-
const entryIndex = i + 1; const entry = candles[entryIndex]; const atr = indicators.atr14; if (!Number.isFinite(atr) || atr <= 0 || !Number.isFinite(entry.open)) continue;
|
| 84 |
-
const stopDistance = atr * atrStopMultiple; const rawEntryPrice = entry.open; const entryPrice = direction === 'LONG' ? rawEntryPrice * (1 + slippageRate) : rawEntryPrice * (1 - slippageRate); const stopLoss = direction === 'LONG' ? entryPrice - stopDistance : entryPrice + stopDistance; const takeProfit = direction === 'LONG' ? entryPrice + stopDistance * rewardRisk : entryPrice - stopDistance * rewardRisk;
|
| 85 |
-
const lastIndex = clampIndex(entryIndex + maxHoldingBars, candles.length - 1); let rawExitPrice = candles[lastIndex].close; let status: HistoricalBacktestTrade['status'] = 'TIMEOUT'; let exitIndex = lastIndex;
|
| 86 |
-
for (let j = entryIndex; j <= lastIndex; j += 1) { const candle = candles[j]; const hitStop = direction === 'LONG' ? candle.low <= stopLoss : candle.high >= stopLoss; const hitTarget = direction === 'LONG' ? candle.high >= takeProfit : candle.low <= takeProfit; if (hitStop) { rawExitPrice = stopLoss; status = 'SL ATINGIDO'; exitIndex = j; break; } if (hitTarget) { rawExitPrice = takeProfit; status = 'TP ATINGIDO'; exitIndex = j; break; } }
|
| 87 |
-
const exitPrice = direction === 'LONG' ? rawExitPrice * (1 - slippageRate) : rawExitPrice * (1 + slippageRate);
|
| 88 |
-
const units = (equity * (riskPerTradePercent / 100)) / stopDistance; const notionalEntry = units * entryPrice; const notionalExit = units * exitPrice; const grossPnl = direction === 'LONG' ? units * (exitPrice - entryPrice) : units * (entryPrice - exitPrice); const fees = (notionalEntry + notionalExit) * feeRate;
|
| 89 |
-
const slippageCost = units * Math.abs(entryPrice - rawEntryPrice) + units * Math.abs(exitPrice - rawExitPrice);
|
| 90 |
-
const holdingBars = exitIndex - entryIndex + 1;
|
| 91 |
-
const fundingRateApplied = hasHistoricalFunding ? historicalFundingSum(historicalFunding, entry.timestamp, candles[exitIndex].timestamp) : fundingRate * (holdingBars / 32);
|
| 92 |
-
const fundingSignedCost = (direction === 'LONG' ? 1 : -1) * notionalEntry * fundingRateApplied;
|
| 93 |
-
const netPnl = grossPnl - fees - fundingSignedCost;
|
| 94 |
-
const grossPnlPercent = notionalEntry > 0 ? grossPnl / notionalEntry * 100 : 0; const feePercent = notionalEntry > 0 ? fees / notionalEntry * 100 : 0; const slippagePercent = notionalEntry > 0 ? slippageCost / notionalEntry * 100 : 0; const fundingPercent = notionalEntry > 0 ? fundingSignedCost / notionalEntry * 100 : 0; const pnlPercent = notionalEntry > 0 ? netPnl / notionalEntry * 100 : 0;
|
| 95 |
-
const riskCapital = equity * (riskPerTradePercent / 100); const grossPnlR = riskCapital > 0 ? grossPnl / riskCapital : 0; const feesR = riskCapital > 0 ? fees / riskCapital : 0; const fundingR = riskCapital > 0 ? fundingSignedCost / riskCapital : 0; const pnlR = riskCapital > 0 ? netPnl / riskCapital : 0;
|
| 96 |
-
let worstIntratradeEquity = equity; const entryFee = notionalEntry * feeRate;
|
| 97 |
-
for (let j = entryIndex; j <= exitIndex; j += 1) { const candle = candles[j]; const adversePrice = direction === 'LONG' ? candle.low : candle.high; const unrealizedPnl = direction === 'LONG' ? units * (adversePrice - entryPrice) : units * (entryPrice - adversePrice); worstIntratradeEquity = Math.min(worstIntratradeEquity, equity + unrealizedPnl - entryFee); }
|
| 98 |
-
if (peak > 0) maxDrawdownPercent = Math.max(maxDrawdownPercent, Math.max(0, (peak - worstIntratradeEquity) / peak * 100));
|
| 99 |
-
equity += netPnl; totalFees += fees; totalSlippage += slippageCost; totalFunding += fundingSignedCost; if (grossPnl > 0) grossProfit += grossPnl; peak = Math.max(peak, equity); const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0; maxDrawdownPercent = Math.max(maxDrawdownPercent, drawdown);
|
| 100 |
-
const date = new Date(entry.timestamp).toISOString().slice(0, 10); trades.push({ id: `${options.symbol.replace('/', '')}-${entry.timestamp}`, timestamp: entry.timestamp, date, symbol: options.symbol, direction, entryPrice, exitPrice, stopLoss, takeProfit, rrRatio: rewardRisk, confidence: confluence.confidence, score: confluence.score, grossPnlPercent, feePercent, slippagePercent, pnlPercent, pnlR, grossPnlR, feesR, fundingPercent, fundingR, status, holdingBars }); equityCurve.push({ date, equity, tradePnl: netPnl, drawdown }); nextFreeIndex = exitIndex + 1;
|
| 101 |
-
}
|
| 102 |
-
const rValues = trades.map(t => t.pnlR); const grossRValues = trades.map(t => t.grossPnlR); const wins = trades.filter(t => t.pnlR > 0); const losses = trades.filter(t => t.pnlR < 0); const netRProfit = wins.reduce((sum, t) => sum + t.pnlR, 0); const netRLoss = Math.abs(losses.reduce((sum, t) => sum + t.pnlR, 0)); const start = candles[0]?.timestamp ?? Date.now(); const end = candles[candles.length - 1]?.timestamp ?? start; const periodDays = Math.max(0, (end - start) / 86_400_000);
|
| 103 |
-
return { symbol: options.symbol, timeframe: '15m', startDate: new Date(start).toISOString(), endDate: new Date(end).toISOString(), periodDays, initialCapital, finalCapital: equity, totalTrades: trades.length, winningTrades: wins.length, losingTrades: losses.length, winRate: trades.length ? wins.length / trades.length * 100 : 0, profitFactor: netRLoss > 0 ? netRProfit / netRLoss : netRProfit > 0 ? Infinity : 0, netProfitPercent: (equity - initialCapital) / initialCapital * 100, grossProfitPercent: initialCapital > 0 ? grossProfit / initialCapital * 100 : 0, totalFeesPercent: initialCapital > 0 ? totalFees / initialCapital * 100 : 0, totalSlippagePercent: initialCapital > 0 ? totalSlippage / initialCapital * 100 : 0, totalFundingPercent: initialCapital > 0 ? totalFunding / initialCapital * 100 : 0, maxDrawdownPercent, sharpeRatio: calculateRatio(rValues), sortinoRatio: calculateRatio(rValues, true), averageRR: mean(rValues), expectancyR: mean(rValues), grossExpectancyR: mean(grossRValues), trades, equityCurve, costModel: { feeBpsPerSide, slippageBpsPerSide: baseSlippageBpsPerSide, latencySlippageBpsPerSide, fundingRatePer8h: hasHistoricalFunding ? 0 : fundingRate, fundingIncluded: hasHistoricalFunding || fundingRate !== 0, fundingSource: hasHistoricalFunding ? 'historical-binance' : fundingRate !== 0 ? 'proxy' : 'none' } };
|
| 104 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/backtest/historicalDataService.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
|
| 3 |
-
const INTERVAL_MS: Record<string, number> = {
|
| 4 |
-
'15m': 15 * 60_000,
|
| 5 |
-
'1h': 60 * 60_000,
|
| 6 |
-
'4h': 4 * 60 * 60_000,
|
| 7 |
-
'1d': 24 * 60 * 60_000,
|
| 8 |
-
};
|
| 9 |
-
|
| 10 |
-
const OKX_BAR: Record<string, string> = {
|
| 11 |
-
'15m': '15m',
|
| 12 |
-
'1h': '1H',
|
| 13 |
-
'4h': '4H',
|
| 14 |
-
'1d': '1Dutc',
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
const TIMEOUT_MS = 10_000;
|
| 18 |
-
|
| 19 |
-
function normalizeSymbol(symbol: string): string {
|
| 20 |
-
return symbol.replace('/', '').toUpperCase();
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
function normalizeOkxSpotInstrument(symbol: string): string {
|
| 24 |
-
return symbol.replace('/', '-').toUpperCase();
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
function parseCandle(row: unknown): Candle | null {
|
| 28 |
-
if (!Array.isArray(row) || row.length < 6) return null;
|
| 29 |
-
const timestamp = Number(row[0]);
|
| 30 |
-
const open = Number(row[1]);
|
| 31 |
-
const high = Number(row[2]);
|
| 32 |
-
const low = Number(row[3]);
|
| 33 |
-
const close = Number(row[4]);
|
| 34 |
-
const volume = Number(row[5]);
|
| 35 |
-
|
| 36 |
-
if (
|
| 37 |
-
!Number.isFinite(timestamp) ||
|
| 38 |
-
!Number.isFinite(open) ||
|
| 39 |
-
!Number.isFinite(high) ||
|
| 40 |
-
!Number.isFinite(low) ||
|
| 41 |
-
!Number.isFinite(close) ||
|
| 42 |
-
!Number.isFinite(volume) ||
|
| 43 |
-
high < Math.max(open, close, low) ||
|
| 44 |
-
low > Math.min(open, close, high) ||
|
| 45 |
-
volume < 0
|
| 46 |
-
) return null;
|
| 47 |
-
|
| 48 |
-
return {
|
| 49 |
-
timestamp,
|
| 50 |
-
timeStr: new Date(timestamp).toISOString(),
|
| 51 |
-
open,
|
| 52 |
-
high,
|
| 53 |
-
low,
|
| 54 |
-
close,
|
| 55 |
-
volume,
|
| 56 |
-
};
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
async function fetchBinanceHistoricalCandles(
|
| 60 |
-
symbol: string,
|
| 61 |
-
interval: string,
|
| 62 |
-
startTime: number,
|
| 63 |
-
endTime: number,
|
| 64 |
-
): Promise<Candle[]> {
|
| 65 |
-
const step = INTERVAL_MS[interval];
|
| 66 |
-
if (!step) throw new Error(`Unsupported historical interval: ${interval}`);
|
| 67 |
-
|
| 68 |
-
const result: Candle[] = [];
|
| 69 |
-
let cursor = startTime;
|
| 70 |
-
const maxPages = Math.ceil((endTime - startTime) / step / 1000) + 2;
|
| 71 |
-
|
| 72 |
-
for (let page = 0; page < maxPages && cursor < endTime; page += 1) {
|
| 73 |
-
const url = new URL('https://api.binance.com/api/v3/klines');
|
| 74 |
-
url.searchParams.set('symbol', normalizeSymbol(symbol));
|
| 75 |
-
url.searchParams.set('interval', interval);
|
| 76 |
-
url.searchParams.set('limit', '1000');
|
| 77 |
-
url.searchParams.set('startTime', String(cursor));
|
| 78 |
-
url.searchParams.set('endTime', String(endTime));
|
| 79 |
-
|
| 80 |
-
const response = await fetch(url, {
|
| 81 |
-
headers: { Accept: 'application/json' },
|
| 82 |
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
| 83 |
-
});
|
| 84 |
-
if (!response.ok) throw new Error(`Binance historical HTTP ${response.status}`);
|
| 85 |
-
const data = await response.json() as unknown;
|
| 86 |
-
if (!Array.isArray(data)) throw new Error('Binance historical payload is invalid');
|
| 87 |
-
if (data.length === 0) break;
|
| 88 |
-
|
| 89 |
-
for (const row of data) {
|
| 90 |
-
const candle = parseCandle(row);
|
| 91 |
-
if (candle && candle.timestamp >= startTime && candle.timestamp < endTime) result.push(candle);
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
const lastTimestamp = Number((data[data.length - 1] as unknown[])[0]);
|
| 95 |
-
if (!Number.isFinite(lastTimestamp) || lastTimestamp < cursor) break;
|
| 96 |
-
cursor = lastTimestamp + step;
|
| 97 |
-
if (data.length < 1000) break;
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
return result;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
async function fetchOkxHistoricalCandles(
|
| 104 |
-
symbol: string,
|
| 105 |
-
interval: string,
|
| 106 |
-
startTime: number,
|
| 107 |
-
endTime: number,
|
| 108 |
-
): Promise<Candle[]> {
|
| 109 |
-
const step = INTERVAL_MS[interval];
|
| 110 |
-
const bar = OKX_BAR[interval];
|
| 111 |
-
if (!step || !bar) throw new Error(`Unsupported OKX historical interval: ${interval}`);
|
| 112 |
-
|
| 113 |
-
const instId = normalizeOkxSpotInstrument(symbol);
|
| 114 |
-
const result: Candle[] = [];
|
| 115 |
-
let after: number | null = null;
|
| 116 |
-
const maxPages = Math.ceil((endTime - startTime) / step / 300) + 4;
|
| 117 |
-
|
| 118 |
-
for (let page = 0; page < maxPages; page += 1) {
|
| 119 |
-
const url = new URL('https://www.okx.com/api/v5/market/history-candles');
|
| 120 |
-
url.searchParams.set('instId', instId);
|
| 121 |
-
url.searchParams.set('bar', bar);
|
| 122 |
-
url.searchParams.set('limit', '300');
|
| 123 |
-
if (after !== null) url.searchParams.set('after', String(after));
|
| 124 |
-
|
| 125 |
-
const response = await fetch(url, {
|
| 126 |
-
headers: { Accept: 'application/json' },
|
| 127 |
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
| 128 |
-
});
|
| 129 |
-
if (!response.ok) throw new Error(`OKX historical HTTP ${response.status}`);
|
| 130 |
-
|
| 131 |
-
const payload = await response.json() as unknown;
|
| 132 |
-
if (!payload || typeof payload !== 'object') throw new Error('OKX historical payload is invalid');
|
| 133 |
-
const body = payload as { code?: string; msg?: string; data?: unknown[] };
|
| 134 |
-
if (body.code !== '0' || !Array.isArray(body.data)) {
|
| 135 |
-
throw new Error(`OKX historical returned code ${body.code ?? 'unknown'}: ${body.msg ?? 'invalid payload'}`);
|
| 136 |
-
}
|
| 137 |
-
if (body.data.length === 0) break;
|
| 138 |
-
|
| 139 |
-
let oldestTimestamp = Number.POSITIVE_INFINITY;
|
| 140 |
-
let added = 0;
|
| 141 |
-
|
| 142 |
-
for (const row of body.data) {
|
| 143 |
-
const candle = parseCandle(row);
|
| 144 |
-
if (!candle) continue;
|
| 145 |
-
oldestTimestamp = Math.min(oldestTimestamp, candle.timestamp);
|
| 146 |
-
if (candle.timestamp >= startTime && candle.timestamp < endTime) {
|
| 147 |
-
result.push(candle);
|
| 148 |
-
added += 1;
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
if (!Number.isFinite(oldestTimestamp) || oldestTimestamp <= startTime) break;
|
| 153 |
-
if (added === 0 && oldestTimestamp < startTime) break;
|
| 154 |
-
|
| 155 |
-
const nextAfter = oldestTimestamp;
|
| 156 |
-
if (after !== null && nextAfter >= after) break;
|
| 157 |
-
after = nextAfter;
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
return result;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
function dedupeAndSort(candles: Candle[]): Candle[] {
|
| 164 |
-
const unique = new Map<number, Candle>();
|
| 165 |
-
for (const candle of candles) unique.set(candle.timestamp, candle);
|
| 166 |
-
return [...unique.values()].sort((a, b) => a.timestamp - b.timestamp);
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
export async function fetchHistoricalBinanceCandles(
|
| 170 |
-
symbol: string,
|
| 171 |
-
interval = '15m',
|
| 172 |
-
startTime: number,
|
| 173 |
-
endTime: number,
|
| 174 |
-
): Promise<Candle[]> {
|
| 175 |
-
const step = INTERVAL_MS[interval];
|
| 176 |
-
if (!step) throw new Error(`Unsupported historical interval: ${interval}`);
|
| 177 |
-
if (endTime <= startTime) throw new Error('Historical endTime must be greater than startTime');
|
| 178 |
-
|
| 179 |
-
try {
|
| 180 |
-
const binanceCandles = await fetchBinanceHistoricalCandles(symbol, interval, startTime, endTime);
|
| 181 |
-
if (binanceCandles.length >= 300) return dedupeAndSort(binanceCandles);
|
| 182 |
-
} catch (error) {
|
| 183 |
-
console.warn(`Binance historical unavailable for ${symbol}:`, error);
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
const okxCandles = await fetchOkxHistoricalCandles(symbol, interval, startTime, endTime);
|
| 187 |
-
const candles = dedupeAndSort(okxCandles);
|
| 188 |
-
if (candles.length === 0) {
|
| 189 |
-
throw new Error(`All historical providers failed for ${symbol}. Binance and OKX returned no usable candles.`);
|
| 190 |
-
}
|
| 191 |
-
return candles;
|
| 192 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/confluence/confluenceEngine.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
| 1 |
-
import type { IndicatorSnapshot } from '../indicators/technicalIndicators.js';
|
| 2 |
-
import type { MarketStructureSnapshot } from '../structure/marketStructure.js';
|
| 3 |
-
import type { SMCAnalysis } from '../structure/smc.js';
|
| 4 |
-
import type { DerivativesAnalysis } from '../derivatives/derivativesAnalysis.js';
|
| 5 |
-
import type { DivergenceAnalysis } from '../divergence/divergenceEngine.js';
|
| 6 |
-
import type { GannAnalysis } from '../gann/gannEngine.js';
|
| 7 |
-
import type { WyckoffAnalysis } from '../wyckoff/wyckoffEngine.js';
|
| 8 |
-
|
| 9 |
-
export type ConfluenceBias = 'bullish' | 'bearish' | 'neutral';
|
| 10 |
-
|
| 11 |
-
export interface TimeframeConfluence {
|
| 12 |
-
timeframe: '15m' | '1h' | '4h' | '1d';
|
| 13 |
-
bias: ConfluenceBias;
|
| 14 |
-
score: number;
|
| 15 |
-
reasons: string[];
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export interface ConfluenceAnalysis {
|
| 19 |
-
bias: ConfluenceBias;
|
| 20 |
-
score: number;
|
| 21 |
-
confidence: number;
|
| 22 |
-
timeframes: TimeframeConfluence[];
|
| 23 |
-
confirmations: string[];
|
| 24 |
-
conflicts: string[];
|
| 25 |
-
entryQuality: 'A+' | 'A' | 'B' | 'C' | 'avoid';
|
| 26 |
-
derivatives: DerivativesAnalysis | null;
|
| 27 |
-
divergenceScore: number;
|
| 28 |
-
gannScore: number;
|
| 29 |
-
wyckoffScore: number;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function signBias(bias: ConfluenceBias): number {
|
| 33 |
-
return bias === 'bullish' ? 1 : bias === 'bearish' ? -1 : 0;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
function indicatorBias(ind: IndicatorSnapshot): { bias: ConfluenceBias; reasons: string[] } {
|
| 37 |
-
const reasons: string[] = [];
|
| 38 |
-
let score = 0;
|
| 39 |
-
if (ind.ema9 > ind.ema21) { score += 1; reasons.push('EMA9 acima da EMA21'); }
|
| 40 |
-
else if (ind.ema9 < ind.ema21) { score -= 1; reasons.push('EMA9 abaixo da EMA21'); }
|
| 41 |
-
if (ind.ema21 > ind.ema50) { score += 1; reasons.push('EMA21 acima da EMA50'); }
|
| 42 |
-
else if (ind.ema21 < ind.ema50) { score -= 1; reasons.push('EMA21 abaixo da EMA50'); }
|
| 43 |
-
if (ind.ema50 > ind.ema200) { score += 1; reasons.push('EMA50 acima da EMA200'); }
|
| 44 |
-
else if (ind.ema50 < ind.ema200) { score -= 1; reasons.push('EMA50 abaixo da EMA200'); }
|
| 45 |
-
if (ind.rsi14 >= 55 && ind.rsi14 <= 70) { score += 1; reasons.push('RSI em regime comprador'); }
|
| 46 |
-
else if (ind.rsi14 <= 45 && ind.rsi14 >= 30) { score -= 1; reasons.push('RSI em regime vendedor'); }
|
| 47 |
-
if (ind.relativeVolume20 >= 1.2) reasons.push('Volume relativo acima da média');
|
| 48 |
-
return { bias: score > 0 ? 'bullish' : score < 0 ? 'bearish' : 'neutral', reasons };
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
export function calculateConfluence(
|
| 52 |
-
inputs: Array<{ timeframe: '15m' | '1h' | '4h' | '1d'; indicators: IndicatorSnapshot; structure: MarketStructureSnapshot; smc: SMCAnalysis; divergences?: DivergenceAnalysis; gann?: GannAnalysis; wyckoff?: WyckoffAnalysis }>,
|
| 53 |
-
derivatives: DerivativesAnalysis | null = null,
|
| 54 |
-
): ConfluenceAnalysis {
|
| 55 |
-
const weights: Record<'15m' | '1h' | '4h' | '1d', number> = { '15m': 1, '1h': 2, '4h': 3, '1d': 4 };
|
| 56 |
-
const timeframes: TimeframeConfluence[] = [];
|
| 57 |
-
let weightedScore = 0;
|
| 58 |
-
let totalWeight = 0;
|
| 59 |
-
const confirmations: string[] = [];
|
| 60 |
-
const conflicts: string[] = [];
|
| 61 |
-
let divergenceScore = 0;
|
| 62 |
-
let gannScore = 0;
|
| 63 |
-
let wyckoffScore = 0;
|
| 64 |
-
|
| 65 |
-
for (const input of inputs) {
|
| 66 |
-
const ib = indicatorBias(input.indicators);
|
| 67 |
-
let score = signBias(input.structure.trend) * 2 + signBias(input.smc.bias) * 2 + signBias(ib.bias);
|
| 68 |
-
const reasons = [...ib.reasons];
|
| 69 |
-
if (input.structure.latestEvent?.type === 'BOS') { score += signBias(input.structure.latestEvent.direction); reasons.push(`BOS ${input.structure.latestEvent.direction}`); }
|
| 70 |
-
if (input.structure.latestEvent?.type === 'CHoCH') { score += signBias(input.structure.latestEvent.direction); reasons.push(`CHoCH ${input.structure.latestEvent.direction}`); }
|
| 71 |
-
if (input.structure.latestSweep) { const sweepBias = input.structure.latestSweep.type === 'low' ? 1 : -1; score += sweepBias; reasons.push(input.structure.latestSweep.type === 'low' ? 'Sweep de sell-side liquidity' : 'Sweep de buy-side liquidity'); }
|
| 72 |
-
if (input.smc.premiumDiscount?.zone === 'discount') score += 1;
|
| 73 |
-
if (input.smc.premiumDiscount?.zone === 'premium') score -= 1;
|
| 74 |
-
|
| 75 |
-
const latestDivergence = input.divergences?.latest;
|
| 76 |
-
if (latestDivergence && latestDivergence.strength >= 25) {
|
| 77 |
-
const dScore = latestDivergence.type.startsWith('bullish') ? 2 : -2;
|
| 78 |
-
score += dScore;
|
| 79 |
-
divergenceScore += dScore * weights[input.timeframe];
|
| 80 |
-
reasons.push(`Divergência ${latestDivergence.type} (${latestDivergence.strength}/100)`);
|
| 81 |
-
}
|
| 82 |
-
if (input.gann) {
|
| 83 |
-
const gScore = Math.max(-1, Math.min(1, input.gann.score));
|
| 84 |
-
score += gScore;
|
| 85 |
-
gannScore += gScore * weights[input.timeframe];
|
| 86 |
-
if (gScore !== 0) reasons.push(`Gann ${input.gann.bias}`);
|
| 87 |
-
}
|
| 88 |
-
if (input.wyckoff) {
|
| 89 |
-
const wScore = Math.max(-2, Math.min(2, input.wyckoff.score));
|
| 90 |
-
score += wScore;
|
| 91 |
-
wyckoffScore += wScore * weights[input.timeframe];
|
| 92 |
-
if (input.wyckoff.latestEvent) reasons.push(`Wyckoff ${input.wyckoff.latestEvent}`);
|
| 93 |
-
reasons.push(`Fase Wyckoff ${input.wyckoff.phase}`);
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
const bias: ConfluenceBias = score >= 2 ? 'bullish' : score <= -2 ? 'bearish' : 'neutral';
|
| 97 |
-
const weight = weights[input.timeframe];
|
| 98 |
-
weightedScore += score * weight;
|
| 99 |
-
totalWeight += weight * 13;
|
| 100 |
-
timeframes.push({ timeframe: input.timeframe, bias, score, reasons });
|
| 101 |
-
if (score >= 5) confirmations.push(`${input.timeframe}: confluência bullish forte`);
|
| 102 |
-
if (score <= -5) confirmations.push(`${input.timeframe}: confluência bearish forte`);
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
if (derivatives) {
|
| 106 |
-
weightedScore += signBias(derivatives.bias) * 2;
|
| 107 |
-
totalWeight += 2 * 13;
|
| 108 |
-
if (derivatives.score > 0) confirmations.push('Derivativos favorecem cenário bullish');
|
| 109 |
-
if (derivatives.score < 0) confirmations.push('Derivativos mostram excesso de posicionamento comprador');
|
| 110 |
-
if (derivatives.score === 0) conflicts.push('Derivativos sem confirmação direcional');
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
const normalized = totalWeight ? weightedScore / totalWeight : 0;
|
| 114 |
-
const score = Math.round(Math.max(-100, Math.min(100, normalized * 100)));
|
| 115 |
-
const bias: ConfluenceBias = score >= 20 ? 'bullish' : score <= -20 ? 'bearish' : 'neutral';
|
| 116 |
-
const confidence = Math.round(Math.min(99, Math.abs(score) + confirmations.length * 3));
|
| 117 |
-
const bullishCount = timeframes.filter(t => t.bias === 'bullish').length;
|
| 118 |
-
const bearishCount = timeframes.filter(t => t.bias === 'bearish').length;
|
| 119 |
-
if (bullishCount > 0 && bearishCount > 0) conflicts.push('Timeframes apresentam conflito de direção');
|
| 120 |
-
if (timeframes.some(t => t.timeframe === '1d' && t.bias !== bias)) conflicts.push('Daily não confirma o viés dominante');
|
| 121 |
-
const entryQuality = confidence >= 80 && conflicts.length === 0 ? 'A+' : confidence >= 70 ? 'A' : confidence >= 55 ? 'B' : confidence >= 35 ? 'C' : 'avoid';
|
| 122 |
-
return { bias, score, confidence, timeframes, confirmations, conflicts, entryQuality, derivatives, divergenceScore, gannScore, wyckoffScore };
|
| 123 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/confluence/index.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
export { calculateConfluence } from './confluenceEngine.js';
|
| 2 |
-
export type { ConfluenceBias, TimeframeConfluence, ConfluenceAnalysis } from './confluenceEngine.js';
|
|
|
|
|
|
|
|
|
server/confluence/marketAnalysisService.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
import { calculateIndicators } from '../indicators/technicalIndicators.js';
|
| 2 |
-
import { getMarketSnapshot } from '../market/marketSnapshotService.js';
|
| 3 |
-
import { analyzeMarketStructure } from '../structure/marketStructure.js';
|
| 4 |
-
import { analyzeSMC } from '../structure/smc.js';
|
| 5 |
-
import { fetchDerivativesSnapshot } from '../derivatives/derivativesClient.js';
|
| 6 |
-
import { analyzeDerivatives } from '../derivatives/derivativesAnalysis.js';
|
| 7 |
-
import { detectDivergences, type DivergenceAnalysis } from '../divergence/divergenceEngine.js';
|
| 8 |
-
import { analyzeGann, type GannAnalysis } from '../gann/gannEngine.js';
|
| 9 |
-
import { analyzeWyckoff, type WyckoffAnalysis } from '../wyckoff/wyckoffEngine.js';
|
| 10 |
-
import { calculateConfluence, type ConfluenceAnalysis } from './confluenceEngine.js';
|
| 11 |
-
import type { MarketTimeframe } from '../market/candleService.js';
|
| 12 |
-
|
| 13 |
-
export interface TimeframeMarketAnalysis {
|
| 14 |
-
timeframe: MarketTimeframe;
|
| 15 |
-
exchange: string;
|
| 16 |
-
latestTimestamp: number;
|
| 17 |
-
indicators: ReturnType<typeof calculateIndicators>;
|
| 18 |
-
structure: ReturnType<typeof analyzeMarketStructure>;
|
| 19 |
-
smc: ReturnType<typeof analyzeSMC>;
|
| 20 |
-
divergences: DivergenceAnalysis;
|
| 21 |
-
gann: GannAnalysis;
|
| 22 |
-
wyckoff: WyckoffAnalysis;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
export interface MarketAnalysis {
|
| 26 |
-
symbol: string;
|
| 27 |
-
fetchedAt: number;
|
| 28 |
-
timeframes: TimeframeMarketAnalysis[];
|
| 29 |
-
derivatives: ReturnType<typeof analyzeDerivatives> | null;
|
| 30 |
-
confluence: ConfluenceAnalysis;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
export async function analyzeMarket(symbol: string, forceRefresh = false): Promise<MarketAnalysis> {
|
| 34 |
-
const snapshot = await getMarketSnapshot(symbol, { forceRefresh });
|
| 35 |
-
const timeframes = (['15m', '1h', '4h', '1d'] as MarketTimeframe[]).map((timeframe) => {
|
| 36 |
-
const series = snapshot.series[timeframe];
|
| 37 |
-
const indicators = calculateIndicators(series.candles);
|
| 38 |
-
const structure = analyzeMarketStructure(series.candles);
|
| 39 |
-
const smc = analyzeSMC(series.candles, structure, indicators);
|
| 40 |
-
const divergences = detectDivergences(series.candles, indicators);
|
| 41 |
-
const gann = analyzeGann(series.candles, structure);
|
| 42 |
-
const wyckoff = analyzeWyckoff(series.candles, structure, indicators);
|
| 43 |
-
return { timeframe, exchange: series.exchange, latestTimestamp: series.latestTimestamp, indicators, structure, smc, divergences, gann, wyckoff };
|
| 44 |
-
});
|
| 45 |
-
|
| 46 |
-
let derivatives: ReturnType<typeof analyzeDerivatives> | null = null;
|
| 47 |
-
try {
|
| 48 |
-
derivatives = analyzeDerivatives(await fetchDerivativesSnapshot(symbol));
|
| 49 |
-
} catch (error) {
|
| 50 |
-
console.warn(`Derivatives unavailable for ${symbol}:`, error);
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
return {
|
| 54 |
-
symbol: snapshot.symbol,
|
| 55 |
-
fetchedAt: snapshot.fetchedAt,
|
| 56 |
-
timeframes,
|
| 57 |
-
derivatives,
|
| 58 |
-
confluence: calculateConfluence(timeframes, derivatives),
|
| 59 |
-
};
|
| 60 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/derivatives/derivativesAnalysis.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
import type { DerivativesSnapshot } from './derivativesClient.js';
|
| 2 |
-
|
| 3 |
-
export type DerivativesBias = 'bullish' | 'bearish' | 'neutral';
|
| 4 |
-
|
| 5 |
-
export interface DerivativesAnalysis {
|
| 6 |
-
bias: DerivativesBias;
|
| 7 |
-
score: number;
|
| 8 |
-
fundingState: 'positive' | 'negative' | 'neutral' | 'unavailable';
|
| 9 |
-
positioningState: 'long-heavy' | 'short-heavy' | 'balanced' | 'unavailable';
|
| 10 |
-
reasons: string[];
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
export function analyzeDerivatives(data: DerivativesSnapshot): DerivativesAnalysis {
|
| 14 |
-
let score = 0;
|
| 15 |
-
const reasons: string[] = [];
|
| 16 |
-
|
| 17 |
-
if (data.fundingRate === null) {
|
| 18 |
-
reasons.push('Funding indisponível');
|
| 19 |
-
} else if (data.fundingRate > 0.0003) {
|
| 20 |
-
score -= 1;
|
| 21 |
-
reasons.push('Funding positivo elevado, favorece risco de longs congestionados');
|
| 22 |
-
} else if (data.fundingRate < -0.0003) {
|
| 23 |
-
score += 1;
|
| 24 |
-
reasons.push('Funding negativo elevado, favorece risco de shorts congestionados');
|
| 25 |
-
} else {
|
| 26 |
-
reasons.push('Funding próximo do neutro');
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
if (data.longShortRatio === null) {
|
| 30 |
-
reasons.push('Long/Short indisponível');
|
| 31 |
-
} else if (data.longShortRatio > 1.2) {
|
| 32 |
-
score -= 1;
|
| 33 |
-
reasons.push('Contas posicionadas majoritariamente em long');
|
| 34 |
-
} else if (data.longShortRatio < 0.83) {
|
| 35 |
-
score += 1;
|
| 36 |
-
reasons.push('Contas posicionadas majoritariamente em short');
|
| 37 |
-
} else {
|
| 38 |
-
reasons.push('Posicionamento Long/Short equilibrado');
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
const bias: DerivativesBias = score > 0 ? 'bullish' : score < 0 ? 'bearish' : 'neutral';
|
| 42 |
-
const fundingState = data.fundingRate === null ? 'unavailable' : data.fundingRate > 0.0001 ? 'positive' : data.fundingRate < -0.0001 ? 'negative' : 'neutral';
|
| 43 |
-
const positioningState = data.longShortRatio === null ? 'unavailable' : data.longShortRatio > 1.2 ? 'long-heavy' : data.longShortRatio < 0.83 ? 'short-heavy' : 'balanced';
|
| 44 |
-
|
| 45 |
-
return { bias, score, fundingState, positioningState, reasons };
|
| 46 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/derivatives/derivativesClient.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
| 1 |
-
export interface DerivativesSnapshot {
|
| 2 |
-
exchange: 'binance-futures' | 'okx-swap';
|
| 3 |
-
symbol: string;
|
| 4 |
-
timestamp: number;
|
| 5 |
-
fundingRate: number | null;
|
| 6 |
-
fundingTime: number | null;
|
| 7 |
-
openInterest: number | null;
|
| 8 |
-
openInterestValue: number | null;
|
| 9 |
-
longShortRatio: number | null;
|
| 10 |
-
longAccountRatio: number | null;
|
| 11 |
-
shortAccountRatio: number | null;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
const TIMEOUT_MS = 10_000;
|
| 15 |
-
|
| 16 |
-
function normalizeSymbol(symbol: string): string {
|
| 17 |
-
return symbol.replace('/', '').toUpperCase();
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
function normalizeOkxSwapInstrument(symbol: string): string {
|
| 21 |
-
const normalized = symbol.replace('/', '-').toUpperCase();
|
| 22 |
-
return normalized.endsWith('-SWAP') ? normalized : `${normalized}-SWAP`;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
async function getJson(url: string): Promise<any> {
|
| 26 |
-
const response = await fetch(url, {
|
| 27 |
-
headers: { Accept: 'application/json' },
|
| 28 |
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
| 29 |
-
});
|
| 30 |
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 31 |
-
return response.json();
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
async function fetchBinanceDerivativesSnapshot(symbol: string): Promise<DerivativesSnapshot> {
|
| 35 |
-
const normalized = normalizeSymbol(symbol);
|
| 36 |
-
const base = 'https://fapi.binance.com';
|
| 37 |
-
const [funding, oi, ratio] = await Promise.all([
|
| 38 |
-
getJson(`${base}/fapi/v1/premiumIndex?symbol=${normalized}`),
|
| 39 |
-
getJson(`${base}/fapi/v1/openInterest?symbol=${normalized}`),
|
| 40 |
-
getJson(`${base}/futures/data/globalLongShortAccountRatio?symbol=${normalized}&period=5m&limit=1`),
|
| 41 |
-
]);
|
| 42 |
-
|
| 43 |
-
const ratioRow = Array.isArray(ratio) ? ratio[0] : null;
|
| 44 |
-
const longShortRatio = ratioRow && Number.isFinite(Number(ratioRow.longShortRatio)) ? Number(ratioRow.longShortRatio) : null;
|
| 45 |
-
const longAccountRatio = ratioRow && Number.isFinite(Number(ratioRow.longAccount)) ? Number(ratioRow.longAccount) : null;
|
| 46 |
-
const shortAccountRatio = ratioRow && Number.isFinite(Number(ratioRow.shortAccount)) ? Number(ratioRow.shortAccount) : null;
|
| 47 |
-
const fundingRate = Number.isFinite(Number(funding?.lastFundingRate)) ? Number(funding.lastFundingRate) : null;
|
| 48 |
-
const openInterest = Number.isFinite(Number(oi?.openInterest)) ? Number(oi.openInterest) : null;
|
| 49 |
-
const markPrice = Number.isFinite(Number(funding?.markPrice)) ? Number(funding.markPrice) : null;
|
| 50 |
-
|
| 51 |
-
return {
|
| 52 |
-
exchange: 'binance-futures',
|
| 53 |
-
symbol: normalized,
|
| 54 |
-
timestamp: Date.now(),
|
| 55 |
-
fundingRate,
|
| 56 |
-
fundingTime: Number.isFinite(Number(funding?.nextFundingTime)) ? Number(funding.nextFundingTime) : null,
|
| 57 |
-
openInterest,
|
| 58 |
-
openInterestValue: openInterest !== null && markPrice !== null ? openInterest * markPrice : null,
|
| 59 |
-
longShortRatio,
|
| 60 |
-
longAccountRatio,
|
| 61 |
-
shortAccountRatio,
|
| 62 |
-
};
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
async function fetchOkxDerivativesSnapshot(symbol: string): Promise<DerivativesSnapshot> {
|
| 66 |
-
const instId = normalizeOkxSwapInstrument(symbol);
|
| 67 |
-
const base = 'https://www.okx.com';
|
| 68 |
-
const [funding, openInterestResponse] = await Promise.all([
|
| 69 |
-
getJson(`${base}/api/v5/public/funding-rate?instId=${encodeURIComponent(instId)}`),
|
| 70 |
-
getJson(`${base}/api/v5/public/open-interest?instType=SWAP&instId=${encodeURIComponent(instId)}`),
|
| 71 |
-
]);
|
| 72 |
-
|
| 73 |
-
if (funding?.code !== '0' || !Array.isArray(funding?.data) || !funding.data[0]) {
|
| 74 |
-
throw new Error(`OKX funding-rate returned an invalid payload for ${instId}`);
|
| 75 |
-
}
|
| 76 |
-
if (openInterestResponse?.code !== '0' || !Array.isArray(openInterestResponse?.data) || !openInterestResponse.data[0]) {
|
| 77 |
-
throw new Error(`OKX open-interest returned an invalid payload for ${instId}`);
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
const fundingRow = funding.data[0];
|
| 81 |
-
const oiRow = openInterestResponse.data[0];
|
| 82 |
-
const fundingRate = Number.isFinite(Number(fundingRow.fundingRate)) ? Number(fundingRow.fundingRate) : null;
|
| 83 |
-
const openInterest = Number.isFinite(Number(oiRow.oi)) ? Number(oiRow.oi) : null;
|
| 84 |
-
const openInterestValue = Number.isFinite(Number(oiRow.oiUsd)) ? Number(oiRow.oiUsd) : null;
|
| 85 |
-
|
| 86 |
-
return {
|
| 87 |
-
exchange: 'okx-swap',
|
| 88 |
-
symbol: normalizeSymbol(symbol),
|
| 89 |
-
timestamp: Date.now(),
|
| 90 |
-
fundingRate,
|
| 91 |
-
fundingTime: Number.isFinite(Number(fundingRow.fundingTime)) ? Number(fundingRow.fundingTime) : null,
|
| 92 |
-
openInterest,
|
| 93 |
-
openInterestValue,
|
| 94 |
-
longShortRatio: null,
|
| 95 |
-
longAccountRatio: null,
|
| 96 |
-
shortAccountRatio: null,
|
| 97 |
-
};
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
export async function fetchDerivativesSnapshot(symbol: string): Promise<DerivativesSnapshot> {
|
| 101 |
-
const errors: string[] = [];
|
| 102 |
-
|
| 103 |
-
try {
|
| 104 |
-
return await fetchBinanceDerivativesSnapshot(symbol);
|
| 105 |
-
} catch (error) {
|
| 106 |
-
errors.push(`Binance Futures: ${String(error)}`);
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
try {
|
| 110 |
-
return await fetchOkxDerivativesSnapshot(symbol);
|
| 111 |
-
} catch (error) {
|
| 112 |
-
errors.push(`OKX SWAP: ${String(error)}`);
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
throw new Error(`All derivatives providers failed. ${errors.join(' | ')}`);
|
| 116 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/derivatives/historicalFundingService.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
export interface HistoricalFundingRate {
|
| 2 |
-
timestamp: number;
|
| 3 |
-
fundingRate: number;
|
| 4 |
-
symbol: string;
|
| 5 |
-
source: 'binance-futures' | 'okx-swap';
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
const TIMEOUT_MS = 10_000;
|
| 9 |
-
const PAGE_LIMIT_BINANCE = 1000;
|
| 10 |
-
const PAGE_LIMIT_OKX = 400;
|
| 11 |
-
const OKX_MAX_HISTORY_MS = 90 * 86_400_000;
|
| 12 |
-
const MAX_RATE = 0.01;
|
| 13 |
-
|
| 14 |
-
function normalizeSymbol(symbol: string): string {
|
| 15 |
-
return symbol.replace('/', '').toUpperCase();
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
function normalizeOkxSwap(symbol: string): string {
|
| 19 |
-
const [base, quote] = symbol.toUpperCase().split('/');
|
| 20 |
-
return `${base}-${quote}-SWAP`;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
async function getJson(url: string): Promise<any> {
|
| 24 |
-
const response = await fetch(url, {
|
| 25 |
-
headers: { Accept: 'application/json' },
|
| 26 |
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
| 27 |
-
});
|
| 28 |
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 29 |
-
return response.json();
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function uniqueSorted(rows: HistoricalFundingRate[]): HistoricalFundingRate[] {
|
| 33 |
-
const unique = new Map<string, HistoricalFundingRate>();
|
| 34 |
-
for (const row of rows) unique.set(`${row.source}:${row.timestamp}`, row);
|
| 35 |
-
return [...unique.values()].sort((a, b) => a.timestamp - b.timestamp);
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
async function fetchBinanceHistoricalFunding(
|
| 39 |
-
symbol: string,
|
| 40 |
-
startTime: number,
|
| 41 |
-
endTime: number,
|
| 42 |
-
): Promise<HistoricalFundingRate[]> {
|
| 43 |
-
const normalized = normalizeSymbol(symbol);
|
| 44 |
-
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime <= startTime) return [];
|
| 45 |
-
|
| 46 |
-
const result: HistoricalFundingRate[] = [];
|
| 47 |
-
let cursor = Math.max(0, Math.floor(startTime));
|
| 48 |
-
const finalTime = Math.floor(endTime);
|
| 49 |
-
|
| 50 |
-
while (cursor <= finalTime) {
|
| 51 |
-
const url = `https://fapi.binance.com/fapi/v1/fundingRate?symbol=${normalized}&startTime=${cursor}&endTime=${finalTime}&limit=${PAGE_LIMIT_BINANCE}`;
|
| 52 |
-
const rows = await getJson(url);
|
| 53 |
-
if (!Array.isArray(rows) || rows.length === 0) break;
|
| 54 |
-
|
| 55 |
-
let newest = cursor;
|
| 56 |
-
for (const row of rows) {
|
| 57 |
-
const timestamp = Number(row?.fundingTime);
|
| 58 |
-
const fundingRate = Number(row?.fundingRate);
|
| 59 |
-
if (!Number.isFinite(timestamp) || !Number.isFinite(fundingRate)) continue;
|
| 60 |
-
if (timestamp < startTime || timestamp > finalTime) continue;
|
| 61 |
-
result.push({
|
| 62 |
-
timestamp,
|
| 63 |
-
fundingRate: Math.max(-MAX_RATE, Math.min(MAX_RATE, fundingRate)),
|
| 64 |
-
symbol: normalized,
|
| 65 |
-
source: 'binance-futures',
|
| 66 |
-
});
|
| 67 |
-
newest = Math.max(newest, timestamp);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
if (rows.length < PAGE_LIMIT_BINANCE || newest <= cursor) break;
|
| 71 |
-
cursor = newest + 1;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
return uniqueSorted(result);
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
/**
|
| 78 |
-
* OKX fallback for environments where Binance historical endpoints are geo-blocked.
|
| 79 |
-
* The public OKX API currently exposes roughly three months through this endpoint.
|
| 80 |
-
*/
|
| 81 |
-
export async function fetchHistoricalOkxFunding(
|
| 82 |
-
symbol: string,
|
| 83 |
-
startTime: number,
|
| 84 |
-
endTime: number,
|
| 85 |
-
): Promise<HistoricalFundingRate[]> {
|
| 86 |
-
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime <= startTime) return [];
|
| 87 |
-
|
| 88 |
-
const instId = normalizeOkxSwap(symbol);
|
| 89 |
-
const effectiveStart = Math.max(Math.floor(startTime), Math.floor(endTime) - OKX_MAX_HISTORY_MS);
|
| 90 |
-
const finalTime = Math.floor(endTime);
|
| 91 |
-
const result: HistoricalFundingRate[] = [];
|
| 92 |
-
let after: number | null = null;
|
| 93 |
-
|
| 94 |
-
for (let page = 0; page < 100; page += 1) {
|
| 95 |
-
const params = new URLSearchParams({ instId, limit: String(PAGE_LIMIT_OKX) });
|
| 96 |
-
if (after !== null) params.set('after', String(after));
|
| 97 |
-
const payload = await getJson(`https://www.okx.com/api/v5/public/funding-rate-history?${params.toString()}`);
|
| 98 |
-
if (String(payload?.code ?? '0') !== '0') throw new Error(`OKX funding history error: ${payload?.msg || payload?.code || 'unknown error'}`);
|
| 99 |
-
const rows = Array.isArray(payload?.data) ? payload.data : [];
|
| 100 |
-
if (!rows.length) break;
|
| 101 |
-
|
| 102 |
-
let oldest = Number.POSITIVE_INFINITY;
|
| 103 |
-
for (const row of rows) {
|
| 104 |
-
const timestamp = Number(row?.fundingTime);
|
| 105 |
-
const fundingRate = Number(row?.realizedRate ?? row?.fundingRate);
|
| 106 |
-
if (!Number.isFinite(timestamp) || !Number.isFinite(fundingRate)) continue;
|
| 107 |
-
oldest = Math.min(oldest, timestamp);
|
| 108 |
-
if (timestamp < effectiveStart || timestamp > finalTime) continue;
|
| 109 |
-
result.push({
|
| 110 |
-
timestamp,
|
| 111 |
-
fundingRate: Math.max(-MAX_RATE, Math.min(MAX_RATE, fundingRate)),
|
| 112 |
-
symbol: instId,
|
| 113 |
-
source: 'okx-swap',
|
| 114 |
-
});
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
if (oldest === Number.POSITIVE_INFINITY || oldest <= effectiveStart || rows.length < PAGE_LIMIT_OKX) break;
|
| 118 |
-
after = oldest;
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
return uniqueSorted(result);
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
/**
|
| 125 |
-
* Backward-compatible entry point used by the server routes.
|
| 126 |
-
* Binance remains canonical. If Binance returns an access error or no data,
|
| 127 |
-
* use real OKX historical funding rather than failing the whole backtest.
|
| 128 |
-
*/
|
| 129 |
-
export async function fetchHistoricalBinanceFunding(
|
| 130 |
-
symbol: string,
|
| 131 |
-
startTime: number,
|
| 132 |
-
endTime: number,
|
| 133 |
-
): Promise<HistoricalFundingRate[]> {
|
| 134 |
-
try {
|
| 135 |
-
const binance = await fetchBinanceHistoricalFunding(symbol, startTime, endTime);
|
| 136 |
-
if (binance.length) return binance;
|
| 137 |
-
} catch (error) {
|
| 138 |
-
console.warn(`Binance historical funding unavailable for ${symbol}:`, error);
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
try {
|
| 142 |
-
const okx = await fetchHistoricalOkxFunding(symbol, startTime, endTime);
|
| 143 |
-
if (okx.length) return okx;
|
| 144 |
-
} catch (error) {
|
| 145 |
-
console.warn(`OKX historical funding unavailable for ${symbol}:`, error);
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
return [];
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
export const fetchHistoricalFunding = fetchHistoricalBinanceFunding;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/derivatives/index.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
export { fetchDerivativesSnapshot } from './derivativesClient.js';
|
| 2 |
-
export type { DerivativesSnapshot } from './derivativesClient.js';
|
|
|
|
|
|
|
|
|
server/divergence/divergenceEngine.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import type { IndicatorSnapshot } from '../indicators/technicalIndicators.js';
|
| 3 |
-
|
| 4 |
-
export type DivergenceType = 'bullish-regular' | 'bearish-regular' | 'bullish-hidden' | 'bearish-hidden';
|
| 5 |
-
|
| 6 |
-
export interface DivergenceSignal {
|
| 7 |
-
type: DivergenceType;
|
| 8 |
-
firstIndex: number;
|
| 9 |
-
secondIndex: number;
|
| 10 |
-
firstTimestamp: number;
|
| 11 |
-
secondTimestamp: number;
|
| 12 |
-
priceFirst: number;
|
| 13 |
-
priceSecond: number;
|
| 14 |
-
rsiFirst: number;
|
| 15 |
-
rsiSecond: number;
|
| 16 |
-
strength: number;
|
| 17 |
-
invalidation: number;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
export interface DivergenceAnalysis {
|
| 21 |
-
bullish: DivergenceSignal[];
|
| 22 |
-
bearish: DivergenceSignal[];
|
| 23 |
-
latest: DivergenceSignal | null;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
interface Pivot { index: number; price: number; oscillator: number; timestamp: number; }
|
| 27 |
-
|
| 28 |
-
function localLow(values: number[], i: number, radius = 3): boolean {
|
| 29 |
-
if (i < radius || i >= values.length - radius) return false;
|
| 30 |
-
for (let j = 1; j <= radius; j++) if (values[i] >= values[i - j] || values[i] > values[i + j]) return false;
|
| 31 |
-
return true;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
function localHigh(values: number[], i: number, radius = 3): boolean {
|
| 35 |
-
if (i < radius || i >= values.length - radius) return false;
|
| 36 |
-
for (let j = 1; j <= radius; j++) if (values[i] <= values[i - j] || values[i] < values[i + j]) return false;
|
| 37 |
-
return true;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
function oscillatorSeries(candles: Candle[], fallback: number): number[] {
|
| 41 |
-
const closes = candles.map(c => c.close);
|
| 42 |
-
const result = closes.map(() => fallback);
|
| 43 |
-
let gains = 0;
|
| 44 |
-
let losses = 0;
|
| 45 |
-
for (let i = 1; i < closes.length; i++) {
|
| 46 |
-
const change = closes[i] - closes[i - 1];
|
| 47 |
-
gains = (gains * 13 + Math.max(change, 0)) / 14;
|
| 48 |
-
losses = (losses * 13 + Math.max(-change, 0)) / 14;
|
| 49 |
-
result[i] = losses === 0 ? 100 : 100 - (100 / (1 + gains / losses));
|
| 50 |
-
}
|
| 51 |
-
return result;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
function buildPivots(candles: Candle[], rsi: number[], kind: 'low' | 'high'): Pivot[] {
|
| 55 |
-
const values = candles.map(c => kind === 'low' ? c.low : c.high);
|
| 56 |
-
const pivots: Pivot[] = [];
|
| 57 |
-
for (let i = 3; i < candles.length - 3; i++) {
|
| 58 |
-
const pivot = kind === 'low' ? localLow(values, i) : localHigh(values, i);
|
| 59 |
-
if (pivot) pivots.push({ index: i, price: values[i], oscillator: rsi[i], timestamp: candles[i].timestamp });
|
| 60 |
-
}
|
| 61 |
-
return pivots;
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
export function detectDivergences(candles: Candle[], indicators?: IndicatorSnapshot): DivergenceAnalysis {
|
| 65 |
-
if (candles.length < 30) return { bullish: [], bearish: [], latest: null };
|
| 66 |
-
const fallbackRsi = indicators?.rsi14 ?? 50;
|
| 67 |
-
const rsi = oscillatorSeries(candles, fallbackRsi);
|
| 68 |
-
const lows = buildPivots(candles, rsi, 'low');
|
| 69 |
-
const highs = buildPivots(candles, rsi, 'high');
|
| 70 |
-
const bullish: DivergenceSignal[] = [];
|
| 71 |
-
const bearish: DivergenceSignal[] = [];
|
| 72 |
-
|
| 73 |
-
for (let i = 1; i < lows.length; i++) {
|
| 74 |
-
const a = lows[i - 1], b = lows[i];
|
| 75 |
-
const priceDelta = (b.price - a.price) / a.price;
|
| 76 |
-
const rsiDelta = b.oscillator - a.oscillator;
|
| 77 |
-
const regular = priceDelta < -0.001 && rsiDelta > 2;
|
| 78 |
-
const hidden = priceDelta > 0.001 && rsiDelta < -2;
|
| 79 |
-
if (regular || hidden) {
|
| 80 |
-
const strength = Math.min(100, Math.round(Math.abs(priceDelta) * 2500 + Math.abs(rsiDelta) * 4));
|
| 81 |
-
bullish.push({ type: regular ? 'bullish-regular' : 'bullish-hidden', firstIndex: a.index, secondIndex: b.index, firstTimestamp: a.timestamp, secondTimestamp: b.timestamp, priceFirst: a.price, priceSecond: b.price, rsiFirst: a.oscillator, rsiSecond: b.oscillator, strength, invalidation: b.price });
|
| 82 |
-
}
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
for (let i = 1; i < highs.length; i++) {
|
| 86 |
-
const a = highs[i - 1], b = highs[i];
|
| 87 |
-
const priceDelta = (b.price - a.price) / a.price;
|
| 88 |
-
const rsiDelta = b.oscillator - a.oscillator;
|
| 89 |
-
const regular = priceDelta > 0.001 && rsiDelta < -2;
|
| 90 |
-
const hidden = priceDelta < -0.001 && rsiDelta > 2;
|
| 91 |
-
if (regular || hidden) {
|
| 92 |
-
const strength = Math.min(100, Math.round(Math.abs(priceDelta) * 2500 + Math.abs(rsiDelta) * 4));
|
| 93 |
-
bearish.push({ type: regular ? 'bearish-regular' : 'bearish-hidden', firstIndex: a.index, secondIndex: b.index, firstTimestamp: a.timestamp, secondTimestamp: b.timestamp, priceFirst: a.price, priceSecond: b.price, rsiFirst: a.oscillator, rsiSecond: b.oscillator, strength, invalidation: b.price });
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
const all = [...bullish, ...bearish].sort((a, b) => b.secondTimestamp - a.secondTimestamp);
|
| 98 |
-
return { bullish, bearish, latest: all[0] ?? null };
|
| 99 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/divergence/index.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
export { detectDivergences } from './divergenceEngine.js';
|
| 2 |
-
export type { DivergenceType, DivergenceSignal, DivergenceAnalysis } from './divergenceEngine.js';
|
|
|
|
|
|
|
|
|
server/gann/gannEngine.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import type { MarketStructureSnapshot } from '../structure/marketStructure.js';
|
| 3 |
-
|
| 4 |
-
export interface GannLevel {
|
| 5 |
-
ratio: number;
|
| 6 |
-
price: number;
|
| 7 |
-
relation: 'support' | 'resistance';
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
export interface GannTimingWindow {
|
| 11 |
-
bars: number;
|
| 12 |
-
targetTimestamp: number;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
export interface GannAnalysis {
|
| 16 |
-
anchorLow: number | null;
|
| 17 |
-
anchorHigh: number | null;
|
| 18 |
-
range: number | null;
|
| 19 |
-
levels: GannLevel[];
|
| 20 |
-
timingWindows: GannTimingWindow[];
|
| 21 |
-
bias: 'bullish' | 'bearish' | 'neutral';
|
| 22 |
-
score: number;
|
| 23 |
-
reasons: string[];
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
const RATIOS = [0.25, 0.382, 0.5, 0.618, 0.75, 1, 1.272, 1.618];
|
| 27 |
-
const TIMING_BARS = [9, 18, 27, 36, 45];
|
| 28 |
-
|
| 29 |
-
export function analyzeGann(candles: Candle[], structure: MarketStructureSnapshot): GannAnalysis {
|
| 30 |
-
const swings = structure.swings;
|
| 31 |
-
if (candles.length < 20 || swings.length < 2) {
|
| 32 |
-
return { anchorLow: null, anchorHigh: null, range: null, levels: [], timingWindows: [], bias: 'neutral', score: 0, reasons: ['Dados insuficientes para análise Gann'] };
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
const highs = swings.filter(s => s.type === 'high');
|
| 36 |
-
const lows = swings.filter(s => s.type === 'low');
|
| 37 |
-
const anchorHigh = highs.length ? highs[highs.length - 1].price : Math.max(...candles.map(c => c.high));
|
| 38 |
-
const anchorLow = lows.length ? lows[lows.length - 1].price : Math.min(...candles.map(c => c.low));
|
| 39 |
-
const low = Math.min(anchorLow, anchorHigh);
|
| 40 |
-
const high = Math.max(anchorLow, anchorHigh);
|
| 41 |
-
const range = high - low;
|
| 42 |
-
const price = candles[candles.length - 1].close;
|
| 43 |
-
|
| 44 |
-
const levels = RATIOS.map(ratio => {
|
| 45 |
-
const level = low + range * ratio;
|
| 46 |
-
return { ratio, price: level, relation: level <= price ? 'support' : 'resistance' as 'support' | 'resistance' };
|
| 47 |
-
});
|
| 48 |
-
|
| 49 |
-
const interval = candles.length > 1 ? candles[candles.length - 1].timestamp - candles[candles.length - 2].timestamp : 0;
|
| 50 |
-
const timingWindows = interval > 0 ? TIMING_BARS.map(bars => ({ bars, targetTimestamp: candles[candles.length - 1].timestamp + interval * bars })) : [];
|
| 51 |
-
|
| 52 |
-
const midpoint = low + range * 0.5;
|
| 53 |
-
let score = 0;
|
| 54 |
-
const reasons: string[] = [];
|
| 55 |
-
if (price > midpoint) { score += 1; reasons.push('Preço acima de 50% do range âncora'); }
|
| 56 |
-
else if (price < midpoint) { score -= 1; reasons.push('Preço abaixo de 50% do range âncora'); }
|
| 57 |
-
if (structure.trend === 'bullish') { score += 1; reasons.push('Estrutura confirma viés bullish'); }
|
| 58 |
-
if (structure.trend === 'bearish') { score -= 1; reasons.push('Estrutura confirma viés bearish'); }
|
| 59 |
-
|
| 60 |
-
return { anchorLow: low, anchorHigh: high, range, levels, timingWindows, bias: score > 0 ? 'bullish' : score < 0 ? 'bearish' : 'neutral', score, reasons };
|
| 61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/gann/index.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
export { analyzeGann } from './gannEngine.js';
|
| 2 |
-
export type { GannLevel, GannTimingWindow, GannAnalysis } from './gannEngine.js';
|
|
|
|
|
|
|
|
|
server/indicators/index.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
export { atr, bollinger, calculateIndicators, ema, rsi, volumeSma } from './technicalIndicators.js';
|
| 2 |
-
export type { IndicatorSnapshot } from './technicalIndicators.js';
|
|
|
|
|
|
|
|
|
server/indicators/technicalIndicators.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
|
| 3 |
-
export interface IndicatorSnapshot {
|
| 4 |
-
ema9: number;
|
| 5 |
-
ema21: number;
|
| 6 |
-
ema50: number;
|
| 7 |
-
ema200: number;
|
| 8 |
-
rsi14: number;
|
| 9 |
-
atr14: number;
|
| 10 |
-
bollinger: {
|
| 11 |
-
middle: number;
|
| 12 |
-
upper: number;
|
| 13 |
-
lower: number;
|
| 14 |
-
widthPercent: number;
|
| 15 |
-
};
|
| 16 |
-
volumeSma20: number;
|
| 17 |
-
relativeVolume20: number;
|
| 18 |
-
momentumPercent: number;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
function closes(candles: Candle[]): number[] {
|
| 22 |
-
return candles.map(c => c.close).filter(Number.isFinite);
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
export function ema(values: number[], period: number): number {
|
| 26 |
-
if (values.length === 0) return 0;
|
| 27 |
-
const seed = values.slice(0, Math.min(period, values.length)).reduce((a, b) => a + b, 0) / Math.min(period, values.length);
|
| 28 |
-
if (values.length <= period) return seed;
|
| 29 |
-
const multiplier = 2 / (period + 1);
|
| 30 |
-
let result = seed;
|
| 31 |
-
for (let i = period; i < values.length; i++) result = (values[i] - result) * multiplier + result;
|
| 32 |
-
return result;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
export function rsi(values: number[], period = 14): number {
|
| 36 |
-
if (values.length <= period) return 50;
|
| 37 |
-
let gains = 0;
|
| 38 |
-
let losses = 0;
|
| 39 |
-
for (let i = 1; i <= period; i++) {
|
| 40 |
-
const change = values[i] - values[i - 1];
|
| 41 |
-
if (change >= 0) gains += change; else losses -= change;
|
| 42 |
-
}
|
| 43 |
-
let avgGain = gains / period;
|
| 44 |
-
let avgLoss = losses / period;
|
| 45 |
-
for (let i = period + 1; i < values.length; i++) {
|
| 46 |
-
const change = values[i] - values[i - 1];
|
| 47 |
-
avgGain = (avgGain * (period - 1) + Math.max(change, 0)) / period;
|
| 48 |
-
avgLoss = (avgLoss * (period - 1) + Math.max(-change, 0)) / period;
|
| 49 |
-
}
|
| 50 |
-
if (avgLoss === 0) return 100;
|
| 51 |
-
if (avgGain === 0) return 0;
|
| 52 |
-
const rs = avgGain / avgLoss;
|
| 53 |
-
return 100 - 100 / (1 + rs);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
export function atr(candles: Candle[], period = 14): number {
|
| 57 |
-
if (candles.length < 2) return 0;
|
| 58 |
-
const ranges: number[] = [];
|
| 59 |
-
for (let i = 1; i < candles.length; i++) {
|
| 60 |
-
const c = candles[i];
|
| 61 |
-
const prev = candles[i - 1].close;
|
| 62 |
-
ranges.push(Math.max(c.high - c.low, Math.abs(c.high - prev), Math.abs(c.low - prev)));
|
| 63 |
-
}
|
| 64 |
-
const window = ranges.slice(-period);
|
| 65 |
-
return window.length ? window.reduce((a, b) => a + b, 0) / window.length : 0;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
export function bollinger(values: number[], period = 20, deviations = 2) {
|
| 69 |
-
const window = values.slice(-period);
|
| 70 |
-
if (!window.length) return { middle: 0, upper: 0, lower: 0, widthPercent: 0 };
|
| 71 |
-
const middle = window.reduce((a, b) => a + b, 0) / window.length;
|
| 72 |
-
const variance = window.reduce((sum, value) => sum + (value - middle) ** 2, 0) / window.length;
|
| 73 |
-
const std = Math.sqrt(variance);
|
| 74 |
-
const upper = middle + deviations * std;
|
| 75 |
-
const lower = middle - deviations * std;
|
| 76 |
-
return { middle, upper, lower, widthPercent: middle ? ((upper - lower) / middle) * 100 : 0 };
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
export function volumeSma(candles: Candle[], period = 20): number {
|
| 80 |
-
const values = candles.slice(-period).map(c => c.volume).filter(Number.isFinite);
|
| 81 |
-
return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
export function calculateIndicators(candles: Candle[]): IndicatorSnapshot {
|
| 85 |
-
const values = closes(candles);
|
| 86 |
-
const latest = values.at(-1) ?? 0;
|
| 87 |
-
const previous = values.at(-2) ?? latest;
|
| 88 |
-
const volumeAverage = volumeSma(candles, 20);
|
| 89 |
-
const latestVolume = candles.at(-1)?.volume ?? 0;
|
| 90 |
-
|
| 91 |
-
return {
|
| 92 |
-
ema9: ema(values, 9),
|
| 93 |
-
ema21: ema(values, 21),
|
| 94 |
-
ema50: ema(values, 50),
|
| 95 |
-
ema200: ema(values, 200),
|
| 96 |
-
rsi14: rsi(values, 14),
|
| 97 |
-
atr14: atr(candles, 14),
|
| 98 |
-
bollinger: bollinger(values, 20, 2),
|
| 99 |
-
volumeSma20: volumeAverage,
|
| 100 |
-
relativeVolume20: volumeAverage > 0 ? latestVolume / volumeAverage : 0,
|
| 101 |
-
momentumPercent: previous !== 0 ? ((latest - previous) / previous) * 100 : 0,
|
| 102 |
-
};
|
| 103 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/market/OKX_FIX_PENDING.md
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Runtime validation identified an OKX instrument normalization issue for BTC/USDT. The fallback currently requests BTC-USDT-SWAP and the runtime reported an invalid candle payload. This marker is temporary and must be removed after the exchange client is corrected and CI/runtime validation passes.
|
|
|
|
|
|
server/market/candleService.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import {
|
| 3 |
-
fetchCandlesWithFallback,
|
| 4 |
-
type ExchangeId,
|
| 5 |
-
type RawCandle,
|
| 6 |
-
} from './exchangeClient.js';
|
| 7 |
-
|
| 8 |
-
export type MarketTimeframe = '15m' | '1h' | '4h' | '1d';
|
| 9 |
-
|
| 10 |
-
const INTERVAL_MAP: Record<MarketTimeframe, string> = {
|
| 11 |
-
'15m': '15m',
|
| 12 |
-
'1h': '1h',
|
| 13 |
-
'4h': '4h',
|
| 14 |
-
'1d': '1d',
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
function toCandle(raw: RawCandle): Candle {
|
| 18 |
-
return {
|
| 19 |
-
timestamp: raw.timestamp,
|
| 20 |
-
timeStr: new Date(raw.timestamp).toISOString(),
|
| 21 |
-
open: raw.open,
|
| 22 |
-
high: raw.high,
|
| 23 |
-
low: raw.low,
|
| 24 |
-
close: raw.close,
|
| 25 |
-
volume: raw.volume,
|
| 26 |
-
};
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
function validateCandle(candle: Candle): boolean {
|
| 30 |
-
return Number.isFinite(candle.timestamp)
|
| 31 |
-
&& Number.isFinite(candle.open)
|
| 32 |
-
&& Number.isFinite(candle.high)
|
| 33 |
-
&& Number.isFinite(candle.low)
|
| 34 |
-
&& Number.isFinite(candle.close)
|
| 35 |
-
&& Number.isFinite(candle.volume)
|
| 36 |
-
&& candle.high >= Math.max(candle.open, candle.close, candle.low)
|
| 37 |
-
&& candle.low <= Math.min(candle.open, candle.close, candle.high)
|
| 38 |
-
&& candle.volume >= 0;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
export async function fetchRealCandles(
|
| 42 |
-
symbol: string,
|
| 43 |
-
timeframe: MarketTimeframe,
|
| 44 |
-
limit = 500,
|
| 45 |
-
preferredExchange: ExchangeId = 'binance',
|
| 46 |
-
): Promise<{ exchange: ExchangeId; candles: Candle[] }> {
|
| 47 |
-
const result = await fetchCandlesWithFallback({
|
| 48 |
-
symbol,
|
| 49 |
-
interval: INTERVAL_MAP[timeframe],
|
| 50 |
-
limit,
|
| 51 |
-
}, preferredExchange);
|
| 52 |
-
|
| 53 |
-
const candles = result.candles
|
| 54 |
-
.map(toCandle)
|
| 55 |
-
.filter(validateCandle)
|
| 56 |
-
.sort((a, b) => a.timestamp - b.timestamp);
|
| 57 |
-
|
| 58 |
-
const deduplicated = candles.filter((candle, index) => (
|
| 59 |
-
index === 0 || candle.timestamp !== candles[index - 1].timestamp
|
| 60 |
-
));
|
| 61 |
-
|
| 62 |
-
return { exchange: result.exchange, candles: deduplicated };
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
export async function fetchMultiTimeframeCandles(
|
| 66 |
-
symbol: string,
|
| 67 |
-
limits: Partial<Record<MarketTimeframe, number>> = {},
|
| 68 |
-
): Promise<Record<MarketTimeframe, { exchange: ExchangeId; candles: Candle[] }>> {
|
| 69 |
-
const timeframes: MarketTimeframe[] = ['15m', '1h', '4h', '1d'];
|
| 70 |
-
const results = await Promise.all(
|
| 71 |
-
timeframes.map(async (timeframe) => [
|
| 72 |
-
timeframe,
|
| 73 |
-
await fetchRealCandles(symbol, timeframe, limits[timeframe] ?? 500),
|
| 74 |
-
] as const),
|
| 75 |
-
);
|
| 76 |
-
|
| 77 |
-
return Object.fromEntries(results) as Record<MarketTimeframe, { exchange: ExchangeId; candles: Candle[] }>;
|
| 78 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/market/exchangeClient.okx-fix-notes.md
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Temporary note
|
|
|
|
|
|
server/market/exchangeClient.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
| 1 |
-
export type ExchangeId = 'binance' | 'okx';
|
| 2 |
-
|
| 3 |
-
export interface RawCandle {
|
| 4 |
-
timestamp: number;
|
| 5 |
-
open: number;
|
| 6 |
-
high: number;
|
| 7 |
-
low: number;
|
| 8 |
-
close: number;
|
| 9 |
-
volume: number;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
export interface FetchCandlesOptions {
|
| 13 |
-
symbol: string;
|
| 14 |
-
interval: string;
|
| 15 |
-
limit?: number;
|
| 16 |
-
signal?: AbortSignal;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
| 20 |
-
|
| 21 |
-
function withTimeout(signal?: AbortSignal, timeoutMs = DEFAULT_TIMEOUT_MS): AbortSignal {
|
| 22 |
-
if (signal) return signal;
|
| 23 |
-
return AbortSignal.timeout(timeoutMs);
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
function normalizeBinanceSymbol(symbol: string): string {
|
| 27 |
-
return symbol.replace('/', '').toUpperCase();
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
function normalizeOkxInstrument(symbol: string): string {
|
| 31 |
-
return symbol.replace('/', '-').toUpperCase();
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
function normalizeOkxBar(interval: string): string {
|
| 35 |
-
const bars: Record<string, string> = {
|
| 36 |
-
'15m': '15m',
|
| 37 |
-
'1h': '1H',
|
| 38 |
-
'4h': '4H',
|
| 39 |
-
'1d': '1D',
|
| 40 |
-
};
|
| 41 |
-
return bars[interval] ?? interval;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
async function fetchJson(url: string, signal?: AbortSignal): Promise<unknown> {
|
| 45 |
-
const response = await fetch(url, {
|
| 46 |
-
headers: { Accept: 'application/json' },
|
| 47 |
-
signal: withTimeout(signal),
|
| 48 |
-
});
|
| 49 |
-
|
| 50 |
-
if (!response.ok) {
|
| 51 |
-
throw new Error(`Exchange HTTP ${response.status}: ${response.statusText}`);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
return response.json();
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
export async function fetchBinanceCandles(options: FetchCandlesOptions): Promise<RawCandle[]> {
|
| 58 |
-
const symbol = normalizeBinanceSymbol(options.symbol);
|
| 59 |
-
const limit = Math.min(Math.max(options.limit ?? 500, 1), 1000);
|
| 60 |
-
const url = new URL('https://api.binance.com/api/v3/klines');
|
| 61 |
-
url.searchParams.set('symbol', symbol);
|
| 62 |
-
url.searchParams.set('interval', options.interval);
|
| 63 |
-
url.searchParams.set('limit', String(limit));
|
| 64 |
-
|
| 65 |
-
const data = await fetchJson(url.toString(), options.signal);
|
| 66 |
-
if (!Array.isArray(data)) throw new Error('Binance returned an invalid kline payload');
|
| 67 |
-
|
| 68 |
-
return data.map((row: unknown[]) => ({
|
| 69 |
-
timestamp: Number(row[0]),
|
| 70 |
-
open: Number(row[1]),
|
| 71 |
-
high: Number(row[2]),
|
| 72 |
-
low: Number(row[3]),
|
| 73 |
-
close: Number(row[4]),
|
| 74 |
-
volume: Number(row[5]),
|
| 75 |
-
}));
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
export async function fetchOkxCandles(options: FetchCandlesOptions): Promise<RawCandle[]> {
|
| 79 |
-
const instId = normalizeOkxInstrument(options.symbol);
|
| 80 |
-
const limit = Math.min(Math.max(options.limit ?? 500, 1), 1000);
|
| 81 |
-
const url = new URL('https://www.okx.com/api/v5/market/candles');
|
| 82 |
-
url.searchParams.set('instId', instId);
|
| 83 |
-
url.searchParams.set('bar', normalizeOkxBar(options.interval));
|
| 84 |
-
url.searchParams.set('limit', String(limit));
|
| 85 |
-
|
| 86 |
-
const payload = await fetchJson(url.toString(), options.signal) as { code?: string; data?: string[][]; msg?: string };
|
| 87 |
-
if (payload.code !== '0' || !Array.isArray(payload.data)) {
|
| 88 |
-
throw new Error(`OKX returned an invalid candle payload for ${instId}: ${payload.msg ?? 'unknown error'}`);
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
return payload.data
|
| 92 |
-
.map((row) => ({
|
| 93 |
-
timestamp: Number(row[0]),
|
| 94 |
-
open: Number(row[1]),
|
| 95 |
-
high: Number(row[2]),
|
| 96 |
-
low: Number(row[3]),
|
| 97 |
-
close: Number(row[4]),
|
| 98 |
-
volume: Number(row[5]),
|
| 99 |
-
}))
|
| 100 |
-
.sort((a, b) => a.timestamp - b.timestamp);
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
export async function fetchCandlesWithFallback(
|
| 104 |
-
options: FetchCandlesOptions,
|
| 105 |
-
preferred: ExchangeId = 'binance',
|
| 106 |
-
): Promise<{ exchange: ExchangeId; candles: RawCandle[] }> {
|
| 107 |
-
const order: ExchangeId[] = preferred === 'binance' ? ['binance', 'okx'] : ['okx', 'binance'];
|
| 108 |
-
const errors: string[] = [];
|
| 109 |
-
|
| 110 |
-
for (const exchange of order) {
|
| 111 |
-
try {
|
| 112 |
-
const candles = exchange === 'binance'
|
| 113 |
-
? await fetchBinanceCandles(options)
|
| 114 |
-
: await fetchOkxCandles(options);
|
| 115 |
-
if (candles.length === 0) throw new Error(`${exchange} returned no candles`);
|
| 116 |
-
return { exchange, candles };
|
| 117 |
-
} catch (error) {
|
| 118 |
-
errors.push(`${exchange}: ${String(error)}`);
|
| 119 |
-
}
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
throw new Error(`All candle providers failed. ${errors.join(' | ')}`);
|
| 123 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/market/marketCache.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
import type { MultiTimeframeSnapshot } from './timeframeService.js';
|
| 2 |
-
|
| 3 |
-
interface CacheEntry {
|
| 4 |
-
snapshot: MultiTimeframeSnapshot;
|
| 5 |
-
expiresAt: number;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
const cache = new Map<string, CacheEntry>();
|
| 9 |
-
const DEFAULT_TTL_MS = 15_000;
|
| 10 |
-
const MAX_ENTRIES = 100;
|
| 11 |
-
|
| 12 |
-
function normalizeSymbol(symbol: string): string {
|
| 13 |
-
return symbol.trim().toUpperCase();
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export function getCachedMarketSnapshot(symbol: string): MultiTimeframeSnapshot | null {
|
| 17 |
-
const key = normalizeSymbol(symbol);
|
| 18 |
-
const entry = cache.get(key);
|
| 19 |
-
if (!entry) return null;
|
| 20 |
-
|
| 21 |
-
if (Date.now() >= entry.expiresAt) {
|
| 22 |
-
cache.delete(key);
|
| 23 |
-
return null;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
return entry.snapshot;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export function setCachedMarketSnapshot(
|
| 30 |
-
symbol: string,
|
| 31 |
-
snapshot: MultiTimeframeSnapshot,
|
| 32 |
-
ttlMs = DEFAULT_TTL_MS,
|
| 33 |
-
): void {
|
| 34 |
-
const key = normalizeSymbol(symbol);
|
| 35 |
-
cache.delete(key);
|
| 36 |
-
cache.set(key, {
|
| 37 |
-
snapshot,
|
| 38 |
-
expiresAt: Date.now() + Math.max(ttlMs, 1_000),
|
| 39 |
-
});
|
| 40 |
-
|
| 41 |
-
while (cache.size > MAX_ENTRIES) {
|
| 42 |
-
const oldestKey = cache.keys().next().value as string | undefined;
|
| 43 |
-
if (!oldestKey) break;
|
| 44 |
-
cache.delete(oldestKey);
|
| 45 |
-
}
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
export function invalidateMarketSnapshot(symbol?: string): void {
|
| 49 |
-
if (symbol) {
|
| 50 |
-
cache.delete(normalizeSymbol(symbol));
|
| 51 |
-
return;
|
| 52 |
-
}
|
| 53 |
-
cache.clear();
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
export function getMarketCacheStats(): { entries: number; symbols: string[] } {
|
| 57 |
-
return {
|
| 58 |
-
entries: cache.size,
|
| 59 |
-
symbols: [...cache.keys()],
|
| 60 |
-
};
|
| 61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/market/marketSnapshotService.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
import { getCachedMarketSnapshot, setCachedMarketSnapshot } from './marketCache.js';
|
| 2 |
-
import { loadMarketSnapshot, type MultiTimeframeSnapshot } from './timeframeService.js';
|
| 3 |
-
|
| 4 |
-
export async function getMarketSnapshot(
|
| 5 |
-
symbol: string,
|
| 6 |
-
options: {
|
| 7 |
-
forceRefresh?: boolean;
|
| 8 |
-
limits?: Parameters<typeof loadMarketSnapshot>[1];
|
| 9 |
-
} = {},
|
| 10 |
-
): Promise<MultiTimeframeSnapshot> {
|
| 11 |
-
if (!options.forceRefresh) {
|
| 12 |
-
const cached = getCachedMarketSnapshot(symbol);
|
| 13 |
-
if (cached) return cached;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
const snapshot = await loadMarketSnapshot(symbol, options.limits);
|
| 17 |
-
setCachedMarketSnapshot(symbol, snapshot);
|
| 18 |
-
return snapshot;
|
| 19 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/market/timeframeService.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import { fetchMultiTimeframeCandles, type MarketTimeframe } from './candleService.js';
|
| 3 |
-
import type { ExchangeId } from './exchangeClient.js';
|
| 4 |
-
|
| 5 |
-
export interface TimeframeSeries {
|
| 6 |
-
timeframe: MarketTimeframe;
|
| 7 |
-
exchange: ExchangeId;
|
| 8 |
-
candles: Candle[];
|
| 9 |
-
latestTimestamp: number;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
export interface MultiTimeframeSnapshot {
|
| 13 |
-
symbol: string;
|
| 14 |
-
fetchedAt: number;
|
| 15 |
-
series: Record<MarketTimeframe, TimeframeSeries>;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export async function loadMarketSnapshot(
|
| 19 |
-
symbol: string,
|
| 20 |
-
limits: Partial<Record<MarketTimeframe, number>> = {},
|
| 21 |
-
): Promise<MultiTimeframeSnapshot> {
|
| 22 |
-
const raw = await fetchMultiTimeframeCandles(symbol, limits);
|
| 23 |
-
const series = {} as Record<MarketTimeframe, TimeframeSeries>;
|
| 24 |
-
|
| 25 |
-
for (const timeframe of ['15m', '1h', '4h', '1d'] as MarketTimeframe[]) {
|
| 26 |
-
const candles = raw[timeframe].candles;
|
| 27 |
-
series[timeframe] = {
|
| 28 |
-
timeframe,
|
| 29 |
-
exchange: raw[timeframe].exchange,
|
| 30 |
-
candles,
|
| 31 |
-
latestTimestamp: candles.at(-1)?.timestamp ?? 0,
|
| 32 |
-
};
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
return {
|
| 36 |
-
symbol,
|
| 37 |
-
fetchedAt: Date.now(),
|
| 38 |
-
series,
|
| 39 |
-
};
|
| 40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/paper/paperTradingEngine.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import type { TradeSignal } from '../signal/signalEngine.js';
|
| 3 |
-
|
| 4 |
-
export type PaperPositionStatus = 'OPEN' | 'CLOSED';
|
| 5 |
-
export type PaperCloseReason = 'TP1' | 'TP2' | 'TP3' | 'STOP' | 'INVALIDATION' | 'MANUAL' | 'TIMEOUT';
|
| 6 |
-
|
| 7 |
-
export interface PaperAccount { initialCapital:number; equity:number; realizedPnl:number; unrealizedPnl:number; feesPaid:number; slippagePaid:number; peakEquity:number; maxDrawdownPercent:number; halted:boolean; lastMarkPrice:number|null; }
|
| 8 |
-
export interface PaperPosition { id:string; symbol:string; direction:'LONG'|'SHORT'; status:PaperPositionStatus; openedAt:number; closedAt:number|null; entryPrice:number; quantity:number; notional:number; stopLoss:number; initialStopLoss:number; takeProfits:{tp1:number;tp2:number;tp3:number}; remainingQuantity:number; realizedPnl:number; unrealizedPnl:number; feesPaid:number; slippagePaid:number; closeReason:PaperCloseReason|null; tp1Hit:boolean; tp2Hit:boolean; breakevenActivated:boolean; lastMarkPrice:number|null; }
|
| 9 |
-
export interface PaperTradingConfig { initialCapital?:number; feeBpsPerSide?:number; slippageBpsPerSide?:number; maxRiskPercent?:number; maxOpenPositions?:number; maxDrawdownPercent?:number; partialTpPercent?:number; }
|
| 10 |
-
export interface PaperTradingStats { totalTrades:number; winningTrades:number; losingTrades:number; breakevenTrades:number; winRatePercent:number; profitFactor:number; netPnl:number; netPnlPercent:number; averageTradePnl:number; averageR:number; bestTradePnl:number; worstTradePnl:number; tp1HitRatePercent:number; tp2HitRatePercent:number; halted:boolean; }
|
| 11 |
-
export interface PaperTradingState { account:PaperAccount; positions:PaperPosition[]; history:PaperPosition[]; stats:PaperTradingStats; }
|
| 12 |
-
const clamp=(v:number,a:number,b:number)=>Math.max(a,Math.min(b,v));
|
| 13 |
-
const positive=(v:number)=>Number.isFinite(v)&&v>0;
|
| 14 |
-
const round=(v:number,d=8)=>Math.round(v*10**d)/10**d;
|
| 15 |
-
export class PaperTradingEngine {
|
| 16 |
-
private readonly config:Required<PaperTradingConfig>; private state:PaperTradingState;
|
| 17 |
-
constructor(config:PaperTradingConfig={}){this.config={initialCapital:positive(config.initialCapital??10000)?config.initialCapital??10000:10000,feeBpsPerSide:clamp(config.feeBpsPerSide??5,0,100),slippageBpsPerSide:clamp(config.slippageBpsPerSide??2,0,100),maxRiskPercent:clamp(config.maxRiskPercent??1,.1,2),maxOpenPositions:Math.max(1,Math.floor(config.maxOpenPositions??1)),maxDrawdownPercent:clamp(config.maxDrawdownPercent??10,1,100),partialTpPercent:clamp(config.partialTpPercent??33.333333,10,90)};this.state=this.emptyState();}
|
| 18 |
-
private emptyStats():PaperTradingStats{return{totalTrades:0,winningTrades:0,losingTrades:0,breakevenTrades:0,winRatePercent:0,profitFactor:0,netPnl:0,netPnlPercent:0,averageTradePnl:0,averageR:0,bestTradePnl:0,worstTradePnl:0,tp1HitRatePercent:0,tp2HitRatePercent:0,halted:false};}
|
| 19 |
-
private emptyState():PaperTradingState{const capital=this.config.initialCapital;return{account:{initialCapital:capital,equity:capital,realizedPnl:0,unrealizedPnl:0,feesPaid:0,slippagePaid:0,peakEquity:capital,maxDrawdownPercent:0,halted:false,lastMarkPrice:null},positions:[],history:[],stats:this.emptyStats()};}
|
| 20 |
-
getState(){return structuredClone(this.state);}
|
| 21 |
-
reset(){this.state=this.emptyState();return this.getState();}
|
| 22 |
-
openFromSignal(signal:TradeSignal,timestamp=Date.now()){if(signal.direction==='NO TRADE'||!signal.entryZone||!signal.stopLoss||!signal.takeProfits||this.state.account.halted||this.state.positions.length>=this.config.maxOpenPositions)return null;const riskPct=Math.min(signal.riskPercent,this.config.maxRiskPercent),riskCash=this.state.account.equity*riskPct/100,entry=signal.entryZone.reference,distance=Math.abs(entry-signal.stopLoss);if(!positive(entry)||!positive(distance)||riskCash<=0)return null;const quantity=riskCash/distance,notional=quantity*entry,entrySlip=entry*this.config.slippageBpsPerSide/10000,executedEntry=signal.direction==='LONG'?entry+entrySlip:entry-entrySlip,fee=notional*this.config.feeBpsPerSide/10000,slippageCost=quantity*Math.abs(executedEntry-entry);const p:PaperPosition={id:`paper-${signal.symbol.replace('/','-')}-${timestamp}-${Math.random().toString(36).slice(2,8)}`,symbol:signal.symbol,direction:signal.direction,status:'OPEN',openedAt:timestamp,closedAt:null,entryPrice:round(executedEntry),quantity:round(quantity),notional:round(notional,4),stopLoss:signal.stopLoss,initialStopLoss:signal.stopLoss,takeProfits:signal.takeProfits,remainingQuantity:round(quantity),realizedPnl:round(-fee-slippageCost),unrealizedPnl:0,feesPaid:round(fee),slippagePaid:round(slippageCost),closeReason:null,tp1Hit:false,tp2Hit:false,breakevenActivated:false,lastMarkPrice:round(executedEntry)};this.state.positions.push(p);this.state.account.realizedPnl-=fee+slippageCost;this.state.account.feesPaid+=fee;this.state.account.slippagePaid+=slippageCost;this.markToMarket(executedEntry);return structuredClone(p);}
|
| 23 |
-
processCandle(candle:Candle,timestamp=candle.timestamp){for(const p of [...this.state.positions]){const hit=this.firstHit(p,candle);if(!hit){if(timestamp-p.openedAt>=32*15*60_000)this.executeClose(p,candle.close,'TIMEOUT',timestamp);continue;}if(hit.reason==='STOP')this.executeClose(p,hit.price,'STOP',timestamp);else if(hit.reason==='TP1'&&!p.tp1Hit){this.executePartialClose(p,hit.price,'TP1',timestamp);p.tp1Hit=true;p.breakevenActivated=true;p.stopLoss=p.entryPrice;}else if(hit.reason==='TP2'&&p.tp1Hit&&!p.tp2Hit){this.executePartialClose(p,hit.price,'TP2',timestamp);p.tp2Hit=true;p.stopLoss=p.entryPrice;}else if(hit.reason==='TP3'&&p.tp2Hit)this.executeClose(p,hit.price,'TP3',timestamp);}this.markToMarket(candle.close,timestamp);}
|
| 24 |
-
markToMarket(price:number,_timestamp=Date.now()){if(!positive(price))return;let unrealized=0;for(const p of this.state.positions){p.lastMarkPrice=round(price);p.unrealizedPnl=round(p.direction==='LONG'?(price-p.entryPrice)*p.remainingQuantity:(p.entryPrice-price)*p.remainingQuantity);unrealized+=p.unrealizedPnl;}this.state.account.unrealizedPnl=round(unrealized);this.state.account.equity=round(this.state.account.initialCapital+this.state.account.realizedPnl+unrealized,4);this.state.account.lastMarkPrice=round(price);this.state.account.peakEquity=Math.max(this.state.account.peakEquity,this.state.account.equity);const dd=this.state.account.peakEquity>0?(this.state.account.peakEquity-this.state.account.equity)/this.state.account.peakEquity*100:0;this.state.account.maxDrawdownPercent=Math.max(this.state.account.maxDrawdownPercent,dd);if(this.state.account.maxDrawdownPercent>=this.config.maxDrawdownPercent)this.state.account.halted=true;this.recalculateStats();}
|
| 25 |
-
close(positionId:string,price:number,reason:PaperCloseReason='MANUAL',timestamp=Date.now()){const p=this.state.positions.find(x=>x.id===positionId);if(!p||!positive(price))return null;this.executeClose(p,price,reason,timestamp);this.markToMarket(price,timestamp);return structuredClone(p);}
|
| 26 |
-
private firstHit(p:PaperPosition,c:Candle){if(p.direction==='LONG'){if(c.low<=p.stopLoss)return{price:p.stopLoss,reason:'STOP' as PaperCloseReason};if(!p.tp1Hit&&c.high>=p.takeProfits.tp1)return{price:p.takeProfits.tp1,reason:'TP1' as PaperCloseReason};if(p.tp1Hit&&!p.tp2Hit&&c.high>=p.takeProfits.tp2)return{price:p.takeProfits.tp2,reason:'TP2' as PaperCloseReason};if(p.tp2Hit&&c.high>=p.takeProfits.tp3)return{price:p.takeProfits.tp3,reason:'TP3' as PaperCloseReason};}else{if(c.high>=p.stopLoss)return{price:p.stopLoss,reason:'STOP' as PaperCloseReason};if(!p.tp1Hit&&c.low<=p.takeProfits.tp1)return{price:p.takeProfits.tp1,reason:'TP1' as PaperCloseReason};if(p.tp1Hit&&!p.tp2Hit&&c.low<=p.takeProfits.tp2)return{price:p.takeProfits.tp2,reason:'TP2' as PaperCloseReason};if(p.tp2Hit&&c.low<=p.takeProfits.tp3)return{price:p.takeProfits.tp3,reason:'TP3' as PaperCloseReason};}return null;}
|
| 27 |
-
private executePartialClose(p:PaperPosition,price:number,_reason:PaperCloseReason,_timestamp:number){if(p.status==='CLOSED')return;const quantity=Math.min(p.remainingQuantity,p.quantity*this.config.partialTpPercent/100);this.executeQuantityClose(p,quantity,price);}
|
| 28 |
-
private executeQuantityClose(p:PaperPosition,quantity:number,price:number){if(p.status==='CLOSED'||quantity<=0)return;const slip=price*this.config.slippageBpsPerSide/10000,executed=p.direction==='LONG'?price-slip:price+slip,gross=p.direction==='LONG'?(executed-p.entryPrice)*quantity:(p.entryPrice-executed)*quantity,fee=Math.abs(executed*quantity)*this.config.feeBpsPerSide/10000,slippageCost=quantity*Math.abs(executed-price),net=gross-fee-slippageCost;p.realizedPnl=round(p.realizedPnl+net);p.feesPaid=round(p.feesPaid+fee);p.slippagePaid=round(p.slippagePaid+slippageCost);p.remainingQuantity=round(Math.max(0,p.remainingQuantity-quantity));p.unrealizedPnl=0;this.state.account.realizedPnl+=net;this.state.account.feesPaid+=fee;this.state.account.slippagePaid+=slippageCost;}
|
| 29 |
-
private executeClose(p:PaperPosition,price:number,reason:PaperCloseReason,timestamp:number){if(p.status==='CLOSED')return;if(p.remainingQuantity>0)this.executeQuantityClose(p,p.remainingQuantity,price);p.remainingQuantity=0;p.unrealizedPnl=0;p.status='CLOSED';p.closedAt=timestamp;p.closeReason=reason;this.state.positions=this.state.positions.filter(x=>x.id!==p.id);this.state.history.push(structuredClone(p));this.recalculateStats();}
|
| 30 |
-
private recalculateStats(){const h=this.state.history,total=h.length,w=h.filter(p=>p.realizedPnl>0).length,l=h.filter(p=>p.realizedPnl<0).length,be=total-w-l,gp=h.reduce((s,p)=>s+Math.max(0,p.realizedPnl),0),gl=Math.abs(h.reduce((s,p)=>s+Math.min(0,p.realizedPnl),0)),net=this.state.account.realizedPnl;const avgR=total?h.reduce((s,p)=>{const risk=Math.abs(p.entryPrice-p.initialStopLoss)*p.quantity;return s+(risk>0?p.realizedPnl/risk:0)},0)/total:0;this.state.stats={totalTrades:total,winningTrades:w,losingTrades:l,breakevenTrades:be,winRatePercent:total?w/total*100:0,profitFactor:gl>0?gp/gl:gp>0?Infinity:0,netPnl:round(net,4),netPnlPercent:this.state.account.initialCapital>0?round(net/this.state.account.initialCapital*100,4):0,averageTradePnl:total?round(net/total,4):0,averageR:round(avgR,4),bestTradePnl:total?round(Math.max(...h.map(p=>p.realizedPnl)),4):0,worstTradePnl:total?round(Math.min(...h.map(p=>p.realizedPnl)),4):0,tp1HitRatePercent:total?h.filter(p=>p.tp1Hit).length/total*100:0,tp2HitRatePercent:total?h.filter(p=>p.tp2Hit).length/total*100:0,halted:this.state.account.halted};}
|
| 31 |
-
}
|
| 32 |
-
export const createPaperTradingEngine=(config:PaperTradingConfig={})=>new PaperTradingEngine(config);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/paper/paperTradingLoop.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
import { fetchRealCandles } from '../market/candleService.js';
|
| 2 |
-
import { analyzeMarket } from '../confluence/marketAnalysisService.js';
|
| 3 |
-
import { generateTradeSignal } from '../signal/signalEngine.js';
|
| 4 |
-
import { PaperTradingEngine } from './paperTradingEngine.js';
|
| 5 |
-
|
| 6 |
-
export interface PaperLoopStatus { running:boolean; symbol:string; intervalMs:number; lastTickAt:number|null; lastCandleTimestamp:number|null; lastError:string|null; }
|
| 7 |
-
|
| 8 |
-
export class PaperTradingLoop {
|
| 9 |
-
private readonly engine: PaperTradingEngine;
|
| 10 |
-
private readonly symbol: string;
|
| 11 |
-
private readonly intervalMs: number;
|
| 12 |
-
private timer: ReturnType<typeof setInterval>|null = null;
|
| 13 |
-
private ticking = false;
|
| 14 |
-
private lastCandleTimestamp:number|null = null;
|
| 15 |
-
private lastTickAt:number|null = null;
|
| 16 |
-
private lastError:string|null = null;
|
| 17 |
-
|
| 18 |
-
constructor(engine=new PaperTradingEngine(),symbol='BTC/USDT',intervalMs=15_000){this.engine=engine;this.symbol=symbol;this.intervalMs=intervalMs;}
|
| 19 |
-
getEngine(){return this.engine;}
|
| 20 |
-
getStatus():PaperLoopStatus{return{running:this.timer!==null,symbol:this.symbol,intervalMs:this.intervalMs,lastTickAt:this.lastTickAt,lastCandleTimestamp:this.lastCandleTimestamp,lastError:this.lastError};}
|
| 21 |
-
async tick(){if(this.ticking)return;this.ticking=true;try{const series=await fetchRealCandles(this.symbol,'15m',500);const candles=series.candles;if(candles.length<50)throw new Error('insufficient_closed_candles');const closed=candles.slice(0,-1);const candle=closed.at(-1);if(!candle)throw new Error('no_closed_candle');this.lastTickAt=Date.now();this.lastError=null;if(this.lastCandleTimestamp===candle.timestamp)return;this.engine.processCandle(candle);this.lastCandleTimestamp=candle.timestamp;if(this.engine.getState().positions.length===0&&!this.engine.getState().account.halted){const analysis=await analyzeMarket(this.symbol);const signal=generateTradeSignal(analysis,candles.slice(0,-1));if(signal.direction!=='NO TRADE')this.engine.openFromSignal(signal,Date.now());}}catch(error){this.lastError=error instanceof Error?error.message:'paper_loop_error';}finally{this.ticking=false;}}
|
| 22 |
-
start(){if(this.timer)return;void this.tick();this.timer=setInterval(()=>void this.tick(),this.intervalMs);}
|
| 23 |
-
stop(){if(this.timer){clearInterval(this.timer);this.timer=null;}}
|
| 24 |
-
}
|
| 25 |
-
export const paperTradingLoop=new PaperTradingLoop();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/monteCarloAnalysis.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestTrade } from '../backtest/historicalBacktest.js';
|
| 2 |
-
|
| 3 |
-
export interface MonteCarloAnalysisResult {
|
| 4 |
-
simulations: number;
|
| 5 |
-
sampleSize: number;
|
| 6 |
-
expectancyR: number;
|
| 7 |
-
medianFinalR: number;
|
| 8 |
-
p05FinalR: number;
|
| 9 |
-
p95FinalR: number;
|
| 10 |
-
medianMaxDrawdownR: number;
|
| 11 |
-
p95MaxDrawdownR: number;
|
| 12 |
-
probabilityOfLossPercent: number;
|
| 13 |
-
probabilityOfDrawdownOver10RPercent: number;
|
| 14 |
-
grade: 'ROBUST' | 'MODERATE' | 'FRAGILE' | 'INSUFFICIENT_DATA';
|
| 15 |
-
warnings: string[];
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 19 |
-
|
| 20 |
-
function rng(seed: number): () => number {
|
| 21 |
-
let s = seed >>> 0;
|
| 22 |
-
return () => { s = (1664525 * s + 1013904223) >>> 0; return s / 4294967296; };
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
function percentile(values: number[], p: number): number {
|
| 26 |
-
if (!values.length) return 0;
|
| 27 |
-
const index = (values.length - 1) * p;
|
| 28 |
-
const lower = Math.floor(index);
|
| 29 |
-
const upper = Math.ceil(index);
|
| 30 |
-
if (lower === upper) return values[lower];
|
| 31 |
-
return values[lower] + (values[upper] - values[lower]) * (index - lower);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
/**
|
| 35 |
-
* Bootstrap Monte Carlo using account-equity compounding.
|
| 36 |
-
* pnlR remains the trade result in units of risk, while the simulated
|
| 37 |
-
* account applies the configured risk percentage to each trade.
|
| 38 |
-
*/
|
| 39 |
-
export function analyzeMonteCarlo(
|
| 40 |
-
trades: HistoricalBacktestTrade[],
|
| 41 |
-
simulations = 3000,
|
| 42 |
-
riskPerTradePercent = 1,
|
| 43 |
-
): MonteCarloAnalysisResult {
|
| 44 |
-
const values = trades.map(t => t.pnlR).filter(Number.isFinite);
|
| 45 |
-
const safeRiskPercent = Number.isFinite(riskPerTradePercent) && riskPerTradePercent > 0 ? riskPerTradePercent : 1;
|
| 46 |
-
const riskFraction = safeRiskPercent / 100;
|
| 47 |
-
|
| 48 |
-
if (values.length < 30) return {
|
| 49 |
-
simulations,
|
| 50 |
-
sampleSize: values.length,
|
| 51 |
-
expectancyR: round(values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0),
|
| 52 |
-
medianFinalR: 0,
|
| 53 |
-
p05FinalR: 0,
|
| 54 |
-
p95FinalR: 0,
|
| 55 |
-
medianMaxDrawdownR: 0,
|
| 56 |
-
p95MaxDrawdownR: 0,
|
| 57 |
-
probabilityOfLossPercent: 0,
|
| 58 |
-
probabilityOfDrawdownOver10RPercent: 0,
|
| 59 |
-
grade: 'INSUFFICIENT_DATA',
|
| 60 |
-
warnings: ['São necessários pelo menos 30 trades para Monte Carlo.'],
|
| 61 |
-
};
|
| 62 |
-
|
| 63 |
-
const random = rng(20260908 + values.length + Math.round(safeRiskPercent * 100));
|
| 64 |
-
const finals: number[] = [];
|
| 65 |
-
const drawdowns: number[] = [];
|
| 66 |
-
let losses = 0;
|
| 67 |
-
let largeDrawdowns = 0;
|
| 68 |
-
|
| 69 |
-
for (let i = 0; i < simulations; i++) {
|
| 70 |
-
let equity = 1;
|
| 71 |
-
let peak = 1;
|
| 72 |
-
let maxDdFraction = 0;
|
| 73 |
-
|
| 74 |
-
for (let j = 0; j < values.length; j++) {
|
| 75 |
-
const tradeR = values[Math.floor(random() * values.length)];
|
| 76 |
-
equity *= Math.max(0, 1 + tradeR * riskFraction);
|
| 77 |
-
peak = Math.max(peak, equity);
|
| 78 |
-
maxDdFraction = Math.max(maxDdFraction, peak > 0 ? (peak - equity) / peak : 0);
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
finals.push((equity - 1) / riskFraction);
|
| 82 |
-
drawdowns.push(maxDdFraction / riskFraction);
|
| 83 |
-
if (equity < 1) losses++;
|
| 84 |
-
if (maxDdFraction / riskFraction > 10) largeDrawdowns++;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
finals.sort((a, b) => a - b);
|
| 88 |
-
drawdowns.sort((a, b) => a - b);
|
| 89 |
-
const probabilityLoss = losses / simulations * 100;
|
| 90 |
-
const probabilityLargeDd = largeDrawdowns / simulations * 100;
|
| 91 |
-
const warnings: string[] = [];
|
| 92 |
-
|
| 93 |
-
if (probabilityLoss > 25) warnings.push(`Monte Carlo: ${round(probabilityLoss, 2)}% das simulações terminaram abaixo do capital inicial.`);
|
| 94 |
-
if (probabilityLargeDd > 25) warnings.push(`Monte Carlo: ${round(probabilityLargeDd, 2)}% das simulações excederam 10R de drawdown.`);
|
| 95 |
-
|
| 96 |
-
const grade = probabilityLoss < 5 && probabilityLargeDd < 10
|
| 97 |
-
? 'ROBUST'
|
| 98 |
-
: probabilityLoss < 15 && probabilityLargeDd < 25
|
| 99 |
-
? 'MODERATE'
|
| 100 |
-
: 'FRAGILE';
|
| 101 |
-
|
| 102 |
-
return {
|
| 103 |
-
simulations,
|
| 104 |
-
sampleSize: values.length,
|
| 105 |
-
expectancyR: round(values.reduce((a, b) => a + b, 0) / values.length),
|
| 106 |
-
medianFinalR: round(percentile(finals, 0.5)),
|
| 107 |
-
p05FinalR: round(percentile(finals, 0.05)),
|
| 108 |
-
p95FinalR: round(percentile(finals, 0.95)),
|
| 109 |
-
medianMaxDrawdownR: round(percentile(drawdowns, 0.5)),
|
| 110 |
-
p95MaxDrawdownR: round(percentile(drawdowns, 0.95)),
|
| 111 |
-
probabilityOfLossPercent: round(probabilityLoss, 2),
|
| 112 |
-
probabilityOfDrawdownOver10RPercent: round(probabilityLargeDd, 2),
|
| 113 |
-
grade,
|
| 114 |
-
warnings,
|
| 115 |
-
};
|
| 116 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/multiRegimeAnalytics.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import type { HistoricalBacktestTrade } from '../backtest/historicalBacktest.js';
|
| 3 |
-
import type { HistoricalFundingRate } from '../derivatives/historicalFundingService.js';
|
| 4 |
-
|
| 5 |
-
export type TrendRegime = 'BULL' | 'BEAR' | 'RANGE';
|
| 6 |
-
export type VolatilityRegime = 'LOW' | 'NORMAL' | 'HIGH';
|
| 7 |
-
export type FundingRegime = 'FAVORABLE' | 'NEUTRAL' | 'ADVERSE' | 'UNAVAILABLE';
|
| 8 |
-
|
| 9 |
-
export interface MultiRegimeCell {
|
| 10 |
-
key: string;
|
| 11 |
-
trend: TrendRegime;
|
| 12 |
-
volatility: VolatilityRegime;
|
| 13 |
-
funding: FundingRegime;
|
| 14 |
-
trades: number;
|
| 15 |
-
wins: number;
|
| 16 |
-
winRatePercent: number;
|
| 17 |
-
netR: number;
|
| 18 |
-
expectancyR: number;
|
| 19 |
-
profitFactor: number;
|
| 20 |
-
maxDrawdownR: number;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
export interface MultiRegimeValidationResult {
|
| 24 |
-
totalCells: number;
|
| 25 |
-
coveredCells: number;
|
| 26 |
-
evaluatedCells: number;
|
| 27 |
-
positiveCells: number;
|
| 28 |
-
coveragePercent: number;
|
| 29 |
-
consistencyPercent: number;
|
| 30 |
-
worstExpectancyR: number;
|
| 31 |
-
status: 'ROBUST' | 'MIXED' | 'FRAGILE' | 'INSUFFICIENT_DATA';
|
| 32 |
-
minimumTradesPerCell: number;
|
| 33 |
-
fundingEvents: number;
|
| 34 |
-
fundingCoveragePercent: number;
|
| 35 |
-
fundingSources: { binance: number; okx: number };
|
| 36 |
-
firstFundingTimestamp: number | null;
|
| 37 |
-
lastFundingTimestamp: number | null;
|
| 38 |
-
warnings: string[];
|
| 39 |
-
cells: MultiRegimeCell[];
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
const round = (value: number, digits = 4) => Number((Number.isFinite(value) ? value : 0).toFixed(digits));
|
| 43 |
-
const mean = (values: number[]) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
| 44 |
-
|
| 45 |
-
function profitFactor(values: number[]): number {
|
| 46 |
-
const wins = values.filter(v => v > 0).reduce((a, b) => a + b, 0);
|
| 47 |
-
const losses = Math.abs(values.filter(v => v < 0).reduce((a, b) => a + b, 0));
|
| 48 |
-
return losses > 0 ? wins / losses : wins > 0 ? Infinity : 0;
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
function drawdownR(values: number[]): number {
|
| 52 |
-
let equity = 0;
|
| 53 |
-
let peak = 0;
|
| 54 |
-
let max = 0;
|
| 55 |
-
for (const value of values) {
|
| 56 |
-
equity += value;
|
| 57 |
-
peak = Math.max(peak, equity);
|
| 58 |
-
max = Math.max(max, peak - equity);
|
| 59 |
-
}
|
| 60 |
-
return max;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
function percentile(values: number[], p: number): number {
|
| 64 |
-
if (!values.length) return 0;
|
| 65 |
-
const sorted = [...values].sort((a, b) => a - b);
|
| 66 |
-
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.floor((sorted.length - 1) * p)))];
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
function latestFundingAtOrBefore(funding: HistoricalFundingRate[], timestamp: number): HistoricalFundingRate | null {
|
| 70 |
-
let latest: HistoricalFundingRate | null = null;
|
| 71 |
-
for (const row of funding) {
|
| 72 |
-
if (row.timestamp > timestamp) break;
|
| 73 |
-
latest = row;
|
| 74 |
-
}
|
| 75 |
-
return latest;
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
function classifyTrend(candles: Candle[], index: number): TrendRegime {
|
| 79 |
-
const start = Math.max(0, index - 63);
|
| 80 |
-
const window = candles.slice(start, index + 1);
|
| 81 |
-
if (window.length < 32) return 'RANGE';
|
| 82 |
-
const closes = window.map(c => c.close);
|
| 83 |
-
const move = closes.at(-1)! / closes[0] - 1;
|
| 84 |
-
const volatility = Math.sqrt(mean(closes.slice(1).map((v, i) => {
|
| 85 |
-
const r = v / closes[i] - 1;
|
| 86 |
-
return r * r;
|
| 87 |
-
})));
|
| 88 |
-
const threshold = Math.max(volatility * 2.5, 0.003);
|
| 89 |
-
if (move >= threshold) return 'BULL';
|
| 90 |
-
if (move <= -threshold) return 'BEAR';
|
| 91 |
-
return 'RANGE';
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
function classifyVolatility(candles: Candle[], index: number): VolatilityRegime {
|
| 95 |
-
const start = Math.max(0, index - 95);
|
| 96 |
-
const window = candles.slice(start, index + 1);
|
| 97 |
-
if (window.length < 32) return 'NORMAL';
|
| 98 |
-
const returns = window.slice(1).map((c, i) => c.close / window[i].close - 1);
|
| 99 |
-
const currentWindow = returns.slice(-31);
|
| 100 |
-
const current = Math.sqrt(mean(currentWindow.map(r => r * r)));
|
| 101 |
-
const history: number[] = [];
|
| 102 |
-
for (let i = 31; i < returns.length; i += 1) {
|
| 103 |
-
const sample = returns.slice(i - 31, i + 1);
|
| 104 |
-
history.push(Math.sqrt(mean(sample.map(r => r * r))));
|
| 105 |
-
}
|
| 106 |
-
const low = percentile(history, 0.33);
|
| 107 |
-
const high = percentile(history, 0.67);
|
| 108 |
-
if (current <= low) return 'LOW';
|
| 109 |
-
if (current >= high) return 'HIGH';
|
| 110 |
-
return 'NORMAL';
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
function classifyFunding(
|
| 114 |
-
funding: HistoricalFundingRate[],
|
| 115 |
-
timestamp: number,
|
| 116 |
-
direction: HistoricalBacktestTrade['direction'],
|
| 117 |
-
): FundingRegime {
|
| 118 |
-
const latest = latestFundingAtOrBefore(funding, timestamp);
|
| 119 |
-
if (!latest) return 'UNAVAILABLE';
|
| 120 |
-
const abs = Math.abs(latest.fundingRate);
|
| 121 |
-
const neutralThreshold = 0.00005;
|
| 122 |
-
if (abs <= neutralThreshold) return 'NEUTRAL';
|
| 123 |
-
const favorable = direction === 'LONG' ? latest.fundingRate < 0 : latest.fundingRate > 0;
|
| 124 |
-
return favorable ? 'FAVORABLE' : 'ADVERSE';
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
export function validateMultiRegime(
|
| 128 |
-
candles: Candle[],
|
| 129 |
-
trades: HistoricalBacktestTrade[],
|
| 130 |
-
historicalFunding: HistoricalFundingRate[] = [],
|
| 131 |
-
minimumTradesPerCell = 10,
|
| 132 |
-
): MultiRegimeValidationResult {
|
| 133 |
-
const safeCandles = [...candles].sort((a, b) => a.timestamp - b.timestamp);
|
| 134 |
-
const safeFunding = [...historicalFunding].filter(x => Number.isFinite(x.timestamp) && Number.isFinite(x.fundingRate)).sort((a, b) => a.timestamp - b.timestamp);
|
| 135 |
-
const trendValues: TrendRegime[] = ['BULL', 'BEAR', 'RANGE'];
|
| 136 |
-
const volatilityValues: VolatilityRegime[] = ['LOW', 'NORMAL', 'HIGH'];
|
| 137 |
-
const fundingValues: FundingRegime[] = safeFunding.length ? ['FAVORABLE', 'NEUTRAL', 'ADVERSE'] : ['FAVORABLE', 'NEUTRAL', 'ADVERSE', 'UNAVAILABLE'];
|
| 138 |
-
const buckets = new Map<string, number[]>();
|
| 139 |
-
|
| 140 |
-
for (const trade of trades) {
|
| 141 |
-
let entryIndex = safeCandles.findIndex(c => c.timestamp === trade.timestamp);
|
| 142 |
-
if (entryIndex < 0) entryIndex = safeCandles.findIndex(c => c.timestamp >= trade.timestamp);
|
| 143 |
-
if (entryIndex <= 0) continue;
|
| 144 |
-
const signalIndex = entryIndex - 1;
|
| 145 |
-
const trend = classifyTrend(safeCandles, signalIndex);
|
| 146 |
-
const volatility = classifyVolatility(safeCandles, signalIndex);
|
| 147 |
-
const funding = classifyFunding(safeFunding, safeCandles[signalIndex].timestamp, trade.direction);
|
| 148 |
-
const key = `${trend}|${volatility}|${funding}`;
|
| 149 |
-
buckets.set(key, [...(buckets.get(key) ?? []), trade.pnlR]);
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
const cells: MultiRegimeCell[] = [];
|
| 153 |
-
for (const trend of trendValues) {
|
| 154 |
-
for (const volatility of volatilityValues) {
|
| 155 |
-
for (const funding of fundingValues) {
|
| 156 |
-
const key = `${trend}|${volatility}|${funding}`;
|
| 157 |
-
const values = buckets.get(key) ?? [];
|
| 158 |
-
const wins = values.filter(v => v > 0).length;
|
| 159 |
-
cells.push({
|
| 160 |
-
key,
|
| 161 |
-
trend,
|
| 162 |
-
volatility,
|
| 163 |
-
funding,
|
| 164 |
-
trades: values.length,
|
| 165 |
-
wins,
|
| 166 |
-
winRatePercent: round(values.length ? wins / values.length * 100 : 0, 2),
|
| 167 |
-
netR: round(values.reduce((a, b) => a + b, 0)),
|
| 168 |
-
expectancyR: round(mean(values)),
|
| 169 |
-
profitFactor: round(profitFactor(values)),
|
| 170 |
-
maxDrawdownR: round(drawdownR(values)),
|
| 171 |
-
});
|
| 172 |
-
}
|
| 173 |
-
}
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
const covered = cells.filter(c => c.trades > 0);
|
| 177 |
-
const evaluated = cells.filter(c => c.trades >= minimumTradesPerCell);
|
| 178 |
-
const positive = evaluated.filter(c => c.expectancyR > 0);
|
| 179 |
-
const coveragePercent = cells.length ? covered.length / cells.length * 100 : 0;
|
| 180 |
-
const consistencyPercent = evaluated.length ? positive.length / evaluated.length * 100 : 0;
|
| 181 |
-
const worstExpectancyR = evaluated.length ? Math.min(...evaluated.map(c => c.expectancyR)) : 0;
|
| 182 |
-
const sourceCounts = {
|
| 183 |
-
binance: safeFunding.filter(x => x.source === 'binance-futures').length,
|
| 184 |
-
okx: safeFunding.filter(x => x.source === 'okx-swap').length,
|
| 185 |
-
};
|
| 186 |
-
const entryTimestamps = trades.map(t => t.timestamp).filter(Number.isFinite);
|
| 187 |
-
const firstEntry = entryTimestamps.length ? Math.min(...entryTimestamps) : null;
|
| 188 |
-
const lastEntry = entryTimestamps.length ? Math.max(...entryTimestamps) : null;
|
| 189 |
-
const fundingAtEntries = trades.filter(t => latestFundingAtOrBefore(safeFunding, t.timestamp)).length;
|
| 190 |
-
const fundingCoveragePercent = trades.length ? fundingAtEntries / trades.length * 100 : 0;
|
| 191 |
-
const warnings: string[] = [];
|
| 192 |
-
if (!safeFunding.length) warnings.push('Nenhum evento histórico de funding disponível para classificar o eixo de funding.');
|
| 193 |
-
if (fundingCoveragePercent < 95 && trades.length) warnings.push(`Cobertura de funding na entrada em ${round(fundingCoveragePercent, 1)}% dos trades.`);
|
| 194 |
-
if (covered.length < Math.min(9, cells.length)) warnings.push('A matriz cobre poucas combinações de regimes; aumentar a janela pode melhorar a representatividade.');
|
| 195 |
-
if (evaluated.some(c => c.expectancyR <= 0)) warnings.push('Existe pelo menos uma combinação de regime relevante com expectancy não positiva.');
|
| 196 |
-
if (evaluated.length < 5) warnings.push(`Menos de cinco células atingiram o mínimo de ${minimumTradesPerCell} trades.`);
|
| 197 |
-
const status = evaluated.length < 5
|
| 198 |
-
? 'INSUFFICIENT_DATA'
|
| 199 |
-
: consistencyPercent >= 75 && worstExpectancyR > 0
|
| 200 |
-
? 'ROBUST'
|
| 201 |
-
: consistencyPercent >= 50
|
| 202 |
-
? 'MIXED'
|
| 203 |
-
: 'FRAGILE';
|
| 204 |
-
|
| 205 |
-
return {
|
| 206 |
-
totalCells: cells.length,
|
| 207 |
-
coveredCells: covered.length,
|
| 208 |
-
evaluatedCells: evaluated.length,
|
| 209 |
-
positiveCells: positive.length,
|
| 210 |
-
coveragePercent: round(coveragePercent, 1),
|
| 211 |
-
consistencyPercent: round(consistencyPercent, 1),
|
| 212 |
-
worstExpectancyR: round(worstExpectancyR),
|
| 213 |
-
status,
|
| 214 |
-
minimumTradesPerCell,
|
| 215 |
-
fundingEvents: safeFunding.length,
|
| 216 |
-
fundingCoveragePercent: round(fundingCoveragePercent, 1),
|
| 217 |
-
fundingSources: sourceCounts,
|
| 218 |
-
firstFundingTimestamp: safeFunding[0]?.timestamp ?? null,
|
| 219 |
-
lastFundingTimestamp: safeFunding.at(-1)?.timestamp ?? null,
|
| 220 |
-
warnings,
|
| 221 |
-
cells,
|
| 222 |
-
};
|
| 223 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/parameterValidation.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestOptions, HistoricalBacktestResult } from '../backtest/historicalBacktest.js';
|
| 2 |
-
import type { Candle } from '../../src/types.js';
|
| 3 |
-
import { runHistoricalBacktest } from '../backtest/historicalBacktest.js';
|
| 4 |
-
import { selectRobustParameters, type RobustParameterSelectionResult } from './robustParameterSelection.js';
|
| 5 |
-
import { analyzeMonteCarlo, type MonteCarloAnalysisResult } from './monteCarloAnalysis.js';
|
| 6 |
-
import { runStressTest, type StressTestResult } from './stressTest.js';
|
| 7 |
-
import { analyzeRegimes, type RegimeAnalyticsResult } from './regimeAnalytics.js';
|
| 8 |
-
import { validateMultiRegime, type MultiRegimeValidationResult } from './multiRegimeAnalytics.js';
|
| 9 |
-
import type { HistoricalFundingRate } from '../derivatives/historicalFundingService.js';
|
| 10 |
-
|
| 11 |
-
export interface OverfittingGuardResult { level: 'LOW' | 'MODERATE' | 'HIGH' | 'INSUFFICIENT_DATA'; score: number; trainExpectancyR: number; holdoutExpectancyR: number; expectancyRetentionPercent: number; trainProfitFactor: number; holdoutProfitFactor: number; profitFactorRetentionPercent: number; tradeCount: number; warnings: string[]; }
|
| 12 |
-
export interface OosSignificanceResult { sampleSize: number; meanR: number; bootstrapCi95R: { low: number; high: number } | null; probabilityPositiveExpectancyPercent: number; probabilityPositiveDeltaPercent: number; baselineDeltaCi95R: { low: number; high: number } | null; status: 'SIGNIFICANT' | 'WEAK' | 'INSUFFICIENT_DATA'; warnings: string[]; }
|
| 13 |
-
export interface RegimeValidationResult { coveredRegimes: number; evaluatedRegimes: number; positiveRegimes: number; coveragePercent: number; consistencyPercent: number; worstExpectancyR: number; status: 'ROBUST' | 'MIXED' | 'FRAGILE' | 'INSUFFICIENT_DATA'; warnings: string[]; buckets: RegimeAnalyticsResult['buckets']; }
|
| 14 |
-
|
| 15 |
-
export interface ParameterValidationResult {
|
| 16 |
-
split: { trainCandles: number; holdoutCandles: number; trainPercent: number };
|
| 17 |
-
optimization: RobustParameterSelectionResult;
|
| 18 |
-
baselineHoldout: HistoricalBacktestResult;
|
| 19 |
-
selectedHoldout: HistoricalBacktestResult | null;
|
| 20 |
-
regimeWindow: HistoricalBacktestResult | null;
|
| 21 |
-
monteCarlo: MonteCarloAnalysisResult | null;
|
| 22 |
-
stressTest: StressTestResult | null;
|
| 23 |
-
comparison: { expectancyDeltaR: number; netProfitDeltaPercent: number; drawdownDeltaPercent: number; selectedBeatsBaseline: boolean };
|
| 24 |
-
overfittingGuard: OverfittingGuardResult;
|
| 25 |
-
significance: OosSignificanceResult;
|
| 26 |
-
regimeValidation: RegimeValidationResult;
|
| 27 |
-
multiRegimeValidation: MultiRegimeValidationResult;
|
| 28 |
-
verdict: 'PASS' | 'CAUTION' | 'REJECT' | 'INSUFFICIENT_DATA';
|
| 29 |
-
warnings: string[];
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 33 |
-
const displayPf = (v: number) => Number.isFinite(v) ? v : v > 0 ? 99 : 0;
|
| 34 |
-
|
| 35 |
-
function assessOverfitting(trainResult: HistoricalBacktestResult, holdoutResult: HistoricalBacktestResult | null, selected: RobustParameterSelectionResult['selected']): OverfittingGuardResult {
|
| 36 |
-
if (!holdoutResult || !selected || trainResult.totalTrades < 30 || holdoutResult.totalTrades < 30) return { level: 'INSUFFICIENT_DATA', score: 0, trainExpectancyR: round(trainResult.expectancyR), holdoutExpectancyR: round(holdoutResult?.expectancyR ?? 0), expectancyRetentionPercent: 0, trainProfitFactor: round(displayPf(trainResult.profitFactor)), holdoutProfitFactor: round(displayPf(holdoutResult?.profitFactor ?? 0)), profitFactorRetentionPercent: 0, tradeCount: holdoutResult?.totalTrades ?? 0, warnings: ['Amostra insuficiente para medir overfitting com confiança.'] };
|
| 37 |
-
const trainExpectancy = trainResult.expectancyR; const holdoutExpectancy = holdoutResult.expectancyR; const trainPf = displayPf(trainResult.profitFactor); const holdoutPf = displayPf(holdoutResult.profitFactor);
|
| 38 |
-
const expectancyRetention = trainExpectancy > 0 ? holdoutExpectancy / trainExpectancy * 100 : holdoutExpectancy > 0 ? 100 : 0; const pfRetention = trainPf > 0 ? holdoutPf / trainPf * 100 : 0;
|
| 39 |
-
const stableBonus = selected.stabilityScore >= selected.rankScore * 0.85 ? 10 : selected.stabilityScore >= selected.rankScore * 0.7 ? 5 : 0; const samplePenalty = holdoutResult.totalTrades < 50 ? 10 : 0; const expectancyPenalty = expectancyRetention < 30 ? 45 : expectancyRetention < 50 ? 30 : expectancyRetention < 70 ? 15 : 0; const pfPenalty = pfRetention < 40 ? 25 : pfRetention < 60 ? 15 : pfRetention < 80 ? 5 : 0;
|
| 40 |
-
const score = Math.max(0, Math.min(100, 100 + stableBonus - samplePenalty - expectancyPenalty - pfPenalty)); const level = score >= 75 ? 'LOW' : score >= 50 ? 'MODERATE' : 'HIGH'; const warnings: string[] = [];
|
| 41 |
-
if (expectancyRetention < 70) warnings.push(`Retenção de expectancy treino→OOS em ${round(expectancyRetention, 1)}%.`); if (pfRetention < 80) warnings.push(`Retenção de Profit Factor treino→OOS em ${round(pfRetention, 1)}%.`); if (holdoutResult.totalTrades < 50) warnings.push('Holdout tem menos de 50 trades; risco estatístico maior.'); if (level === 'HIGH') warnings.push('Risco alto de overfitting: não liberar para uso operacional.');
|
| 42 |
-
return { level, score: round(score, 1), trainExpectancyR: round(trainExpectancy), holdoutExpectancyR: round(holdoutExpectancy), expectancyRetentionPercent: round(expectancyRetention, 1), trainProfitFactor: round(trainPf), holdoutProfitFactor: round(holdoutPf), profitFactorRetentionPercent: round(pfRetention, 1), tradeCount: holdoutResult.totalTrades, warnings };
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
function bootstrapSignificance(values: number[], baselineValues: number[] | null, simulations = 3000): OosSignificanceResult {
|
| 46 |
-
if (values.length < 30) return { sampleSize: values.length, meanR: round(values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0), bootstrapCi95R: null, probabilityPositiveExpectancyPercent: 0, probabilityPositiveDeltaPercent: 0, baselineDeltaCi95R: null, status: 'INSUFFICIENT_DATA', warnings: ['São necessários pelo menos 30 trades OOS.'] };
|
| 47 |
-
let seed = 20260908 + values.length; const random = () => { seed = (1664525 * seed + 1013904223) >>> 0; return seed / 4294967296; };
|
| 48 |
-
const means: number[] = []; const deltas: number[] = []; let positive = 0; let positiveDelta = 0; const observed = values.reduce((a, b) => a + b, 0) / values.length;
|
| 49 |
-
for (let s = 0; s < simulations; s++) { let sum = 0; let deltaSum = 0; for (let i = 0; i < values.length; i++) { const idx = Math.floor(random() * values.length); const v = values[idx]; sum += v; if (baselineValues?.length) deltaSum += v - baselineValues[Math.floor(random() * baselineValues.length)]; } const m = sum / values.length; means.push(m); if (m > 0) positive++; if (baselineValues?.length) { const d = deltaSum / values.length; deltas.push(d); if (d > 0) positiveDelta++; } }
|
| 50 |
-
means.sort((a, b) => a - b); deltas.sort((a, b) => a - b); const percentile = (arr: number[], p: number) => arr.length ? arr[Math.min(arr.length - 1, Math.max(0, Math.floor((arr.length - 1) * p)))] : 0;
|
| 51 |
-
const ci = { low: percentile(means, 0.025), high: percentile(means, 0.975) }; const deltaCi = deltas.length ? { low: percentile(deltas, 0.025), high: percentile(deltas, 0.975) } : null; const pPositive = positive / simulations * 100; const pDelta = baselineValues?.length ? positiveDelta / simulations * 100 : 0; const significant = ci.low > 0 && (!deltaCi || deltaCi.low > 0) && pPositive >= 97.5;
|
| 52 |
-
const warnings: string[] = []; if (ci.low <= 0) warnings.push('IC bootstrap 95% da expectancy inclui zero.'); if (deltaCi && deltaCi.low <= 0) warnings.push('IC bootstrap da vantagem contra baseline inclui zero.');
|
| 53 |
-
return { sampleSize: values.length, meanR: round(observed), bootstrapCi95R: { low: round(ci.low), high: round(ci.high) }, probabilityPositiveExpectancyPercent: round(pPositive, 2), probabilityPositiveDeltaPercent: round(pDelta, 2), baselineDeltaCi95R: deltaCi ? { low: round(deltaCi.low), high: round(deltaCi.high) } : null, status: significant ? 'SIGNIFICANT' : 'WEAK', warnings };
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
function validateRegimes(candles: Candle[], trades: HistoricalBacktestResult['trades']): RegimeValidationResult {
|
| 57 |
-
const analysis = analyzeRegimes(candles, trades); const evaluated = analysis.buckets.filter(b => b.trades >= 10); const positive = evaluated.filter(b => b.expectancyR > 0); const covered = analysis.buckets.filter(b => b.trades > 0).length; const coverage = covered / analysis.buckets.length * 100; const consistency = evaluated.length ? positive.length / evaluated.length * 100 : 0; const worst = evaluated.length ? Math.min(...evaluated.map(b => b.expectancyR)) : 0; const warnings = [...analysis.warnings]; if (covered < 3) warnings.push('Menos de três regimes tiveram operações na janela ampliada.'); if (evaluated.some(b => b.expectancyR <= 0)) warnings.push('Existe pelo menos um regime relevante com expectancy não positiva.'); const status = evaluated.length < 3 ? 'INSUFFICIENT_DATA' : consistency >= 75 && worst > 0 ? 'ROBUST' : consistency >= 50 ? 'MIXED' : 'FRAGILE'; return { coveredRegimes: covered, evaluatedRegimes: evaluated.length, positiveRegimes: positive.length, coveragePercent: round(coverage, 1), consistencyPercent: round(consistency, 1), worstExpectancyR: round(worst), status, warnings, buckets: analysis.buckets };
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
export function validateSelectedParameters(symbol: string, candles: Candle[], baseOptions: Omit<HistoricalBacktestOptions, 'symbol' | 'candles'> = {}, trainPercent = 70): ParameterValidationResult {
|
| 61 |
-
const safePercent = Math.max(60, Math.min(80, trainPercent)); const splitIndex = Math.floor(candles.length * safePercent / 100); const train = candles.slice(0, splitIndex); const holdout = candles.slice(splitIndex); const optimization = selectRobustParameters(symbol, train, baseOptions);
|
| 62 |
-
const baselineHoldout = runHistoricalBacktest({ ...baseOptions, symbol, candles: holdout });
|
| 63 |
-
const selectedHoldout = optimization.selected ? runHistoricalBacktest({ ...baseOptions, symbol, candles: holdout, minScore: optimization.selected.minScore, minConfidence: optimization.selected.minConfidence, atrStopMultiple: optimization.selected.atrStopMultiple, rewardRisk: optimization.selected.rewardRisk, maxHoldingBars: optimization.selected.maxHoldingBars }) : null;
|
| 64 |
-
const trainSelected = optimization.selected ? runHistoricalBacktest({ ...baseOptions, symbol, candles: train, minScore: optimization.selected.minScore, minConfidence: optimization.selected.minConfidence, atrStopMultiple: optimization.selected.atrStopMultiple, rewardRisk: optimization.selected.rewardRisk, maxHoldingBars: optimization.selected.maxHoldingBars }) : null;
|
| 65 |
-
const regimeWindow = optimization.selected ? runHistoricalBacktest({ ...baseOptions, symbol, candles, minScore: optimization.selected.minScore, minConfidence: optimization.selected.minConfidence, atrStopMultiple: optimization.selected.atrStopMultiple, rewardRisk: optimization.selected.rewardRisk, maxHoldingBars: optimization.selected.maxHoldingBars }) : null;
|
| 66 |
-
const monteCarlo = selectedHoldout && selectedHoldout.totalTrades >= 30 ? analyzeMonteCarlo(selectedHoldout.trades, 3000, baseOptions.riskPerTradePercent ?? 1) : null;
|
| 67 |
-
const stressTest = selectedHoldout && selectedHoldout.totalTrades >= 30 ? runStressTest(symbol, holdout, { ...baseOptions, minScore: optimization.selected?.minScore, minConfidence: optimization.selected?.minConfidence, atrStopMultiple: optimization.selected?.atrStopMultiple, rewardRisk: optimization.selected?.rewardRisk, maxHoldingBars: optimization.selected?.maxHoldingBars }) : null;
|
| 68 |
-
const expectancyDeltaR = round((selectedHoldout?.expectancyR ?? 0) - baselineHoldout.expectancyR); const netProfitDeltaPercent = round((selectedHoldout?.netProfitPercent ?? 0) - baselineHoldout.netProfitPercent, 2); const drawdownDeltaPercent = round((selectedHoldout?.maxDrawdownPercent ?? 0) - baselineHoldout.maxDrawdownPercent, 2); const selectedBeatsBaseline = !!selectedHoldout && selectedHoldout.totalTrades >= 30 && selectedHoldout.expectancyR > 0 && selectedHoldout.profitFactor > 1 && selectedHoldout.expectancyR >= baselineHoldout.expectancyR;
|
| 69 |
-
const overfittingGuard = assessOverfitting(trainSelected ?? runHistoricalBacktest({ ...baseOptions, symbol, candles: train }), selectedHoldout, optimization.selected); const significance = bootstrapSignificance(selectedHoldout?.trades.map(t => t.pnlR).filter(Number.isFinite) ?? [], baselineHoldout.trades.map(t => t.pnlR).filter(Number.isFinite)); const regimeValidation = validateRegimes(candles, regimeWindow?.trades ?? []);
|
| 70 |
-
const historicalFunding = (baseOptions.historicalFunding ?? []) as HistoricalFundingRate[];
|
| 71 |
-
const multiRegimeValidation = validateMultiRegime(candles, regimeWindow?.trades ?? [], historicalFunding, 10);
|
| 72 |
-
const warnings: string[] = [...overfittingGuard.warnings, ...significance.warnings, ...regimeValidation.warnings, ...multiRegimeValidation.warnings]; if (train.length < 3000 || holdout.length < 1500) warnings.push('Janela inferior à recomendada para validação multi-regime estendida.'); if (!optimization.selected) warnings.push('Nenhum parâmetro foi selecionado no treino.'); if (selectedHoldout && selectedHoldout.totalTrades < 30) warnings.push('O holdout selecionado tem menos de 30 trades.'); if (monteCarlo?.grade === 'FRAGILE') warnings.push('Monte Carlo classificou a distribuição como FRAGILE.'); if (stressTest?.grade === 'FRAGILE') warnings.push('Stress Test classificou a configuração como FRAGILE.');
|
| 73 |
-
const enough = train.length >= 3000 && holdout.length >= 1500 && !!selectedHoldout && selectedHoldout.totalTrades >= 30; const riskChecksPass = !!monteCarlo && monteCarlo.grade !== 'FRAGILE' && !!stressTest && stressTest.grade !== 'FRAGILE'; const verdict = !enough ? 'INSUFFICIENT_DATA' : !selectedBeatsBaseline || overfittingGuard.level === 'HIGH' || significance.status !== 'SIGNIFICANT' || regimeValidation.status === 'FRAGILE' || regimeValidation.status === 'INSUFFICIENT_DATA' || multiRegimeValidation.status === 'FRAGILE' || multiRegimeValidation.status === 'INSUFFICIENT_DATA' ? 'REJECT' : selectedBeatsBaseline && riskChecksPass && overfittingGuard.level === 'LOW' && regimeValidation.status === 'ROBUST' && multiRegimeValidation.status === 'ROBUST' ? 'PASS' : 'CAUTION';
|
| 74 |
-
return { split: { trainCandles: train.length, holdoutCandles: holdout.length, trainPercent: safePercent }, optimization, baselineHoldout, selectedHoldout, regimeWindow, monteCarlo, stressTest, comparison: { expectancyDeltaR, netProfitDeltaPercent, drawdownDeltaPercent, selectedBeatsBaseline }, overfittingGuard, significance, regimeValidation, multiRegimeValidation, verdict, warnings };
|
| 75 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/quantitativeLab.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestResult } from '../backtest/historicalBacktest.js';
|
| 2 |
-
import type { PaperTradingState } from '../paper/paperTradingEngine.js';
|
| 3 |
-
import type { Candle } from '../../src/types.js';
|
| 4 |
-
import { analyzeRegimes, type RegimeAnalyticsResult } from './regimeAnalytics.js';
|
| 5 |
-
import { analyzeStatistics, type StatisticalAnalysisResult } from './statisticalAnalysis.js';
|
| 6 |
-
import { analyzeRobustness, type RobustnessAnalysisResult } from './robustnessAnalysis.js';
|
| 7 |
-
import { analyzeTimeSeries, type TimeSeriesAnalysisResult } from './timeSeriesAnalysis.js';
|
| 8 |
-
import { runWalkForwardAnalysis, type WalkForwardAnalysisResult } from './walkForwardAnalysis.js';
|
| 9 |
-
import { runStressTest, type StressTestResult } from './stressTest.js';
|
| 10 |
-
import { analyzeMonteCarlo, type MonteCarloAnalysisResult } from './monteCarloAnalysis.js';
|
| 11 |
-
import type { RobustParameterSelectionResult } from './robustParameterSelection.js';
|
| 12 |
-
|
| 13 |
-
export interface QuantMetricSet { trades: number; winRatePercent: number; profitFactor: number; netPnl: number; netPnlPercent: number; expectancyR: number; averageR: number; maxDrawdownPercent: number; bestTradePnl: number; worstTradePnl: number; tp1HitRatePercent: number; tp2HitRatePercent: number; fees: number; slippage: number; }
|
| 14 |
-
export interface QuantitativeLabResult {
|
| 15 |
-
generatedAt: number; symbol: string;
|
| 16 |
-
backtest: QuantMetricSet & { initialCapital: number; finalCapital: number; periodDays: number; sharpeRatio: number; sortinoRatio: number; grossExpectancyR: number; tradeDistribution: { wins: number; losses: number; breakevens: number }; equityCurve: HistoricalBacktestResult['equityCurve'] };
|
| 17 |
-
paper: QuantMetricSet & { initialCapital: number; equity: number; openPositions: number; halted: boolean; history: PaperTradingState['history'] };
|
| 18 |
-
comparison: { netPnlDeltaPercent: number; winRateDeltaPercent: number; expectancyDeltaR: number; drawdownDeltaPercent: number; profitFactorDelta: number; status: 'ALIGNED' | 'DIVERGENT' | 'INSUFFICIENT_DATA'; reasons: string[] };
|
| 19 |
-
quality: { score: number; grade: 'A' | 'B' | 'C' | 'D' | 'INSUFFICIENT_DATA'; checks: string[]; warnings: string[] };
|
| 20 |
-
regimes: RegimeAnalyticsResult; statistics: StatisticalAnalysisResult; robustness: RobustnessAnalysisResult; timeSeries: TimeSeriesAnalysisResult; walkForward: WalkForwardAnalysisResult; stressTest: StressTestResult; monteCarlo: MonteCarloAnalysisResult; parameterSelection: RobustParameterSelectionResult;
|
| 21 |
-
}
|
| 22 |
-
const finite = (v: number, fallback = 0) => Number.isFinite(v) ? v : fallback;
|
| 23 |
-
const round = (v: number, d = 4) => Number(finite(v).toFixed(d));
|
| 24 |
-
const delta = (a: number, b: number) => round(a - b);
|
| 25 |
-
function backtestMetrics(result: HistoricalBacktestResult): QuantitativeLabResult['backtest'] { const wins = result.trades.filter(t => t.pnlR > 0).length; const losses = result.trades.filter(t => t.pnlR < 0).length; return { trades: result.totalTrades, winRatePercent: round(result.winRate), profitFactor: finite(result.profitFactor), netPnl: round(result.finalCapital - result.initialCapital, 2), netPnlPercent: round(result.netProfitPercent), expectancyR: round(result.expectancyR), averageR: round(result.averageRR), maxDrawdownPercent: round(result.maxDrawdownPercent), bestTradePnl: result.trades.length ? round(Math.max(...result.trades.map(t => t.pnlPercent)), 4) : 0, worstTradePnl: result.trades.length ? round(Math.min(...result.trades.map(t => t.pnlPercent)), 4) : 0, tp1HitRatePercent: round(result.trades.filter(t => t.status === 'TP ATINGIDO').length / Math.max(1, result.totalTrades) * 100), tp2HitRatePercent: 0, fees: round(result.totalFeesPercent, 4), slippage: round(result.totalSlippagePercent, 4), initialCapital: result.initialCapital, finalCapital: result.finalCapital, periodDays: round(result.periodDays, 2), sharpeRatio: round(result.sharpeRatio), sortinoRatio: round(result.sortinoRatio), grossExpectancyR: round(result.grossExpectancyR), tradeDistribution: { wins, losses, breakevens: result.totalTrades - wins - losses }, equityCurve: result.equityCurve }; }
|
| 26 |
-
function paperMetrics(state: PaperTradingState): QuantitativeLabResult['paper'] { return { trades: state.stats.totalTrades, winRatePercent: round(state.stats.winRatePercent), profitFactor: finite(state.stats.profitFactor), netPnl: round(state.account.realizedPnl, 2), netPnlPercent: round(state.stats.netPnlPercent), expectancyR: round(state.stats.averageR), averageR: round(state.stats.averageR), maxDrawdownPercent: round(state.account.maxDrawdownPercent), bestTradePnl: round(state.stats.bestTradePnl, 4), worstTradePnl: round(state.stats.worstTradePnl, 4), tp1HitRatePercent: round(state.stats.tp1HitRatePercent), tp2HitRatePercent: round(state.stats.tp2HitRatePercent), fees: round(state.account.feesPaid, 4), slippage: round(state.account.slippagePaid, 4), initialCapital: state.account.initialCapital, equity: state.account.equity, openPositions: state.positions.length, halted: state.account.halted, history: state.history }; }
|
| 27 |
-
function buildComparison(backtest: QuantitativeLabResult['backtest'], paper: QuantitativeLabResult['paper']): QuantitativeLabResult['comparison'] { const common = { netPnlDeltaPercent: delta(paper.netPnlPercent, backtest.netPnlPercent), winRateDeltaPercent: delta(paper.winRatePercent, backtest.winRatePercent), expectancyDeltaR: delta(paper.expectancyR, backtest.expectancyR), drawdownDeltaPercent: delta(paper.maxDrawdownPercent, backtest.maxDrawdownPercent), profitFactorDelta: delta(paper.profitFactor, backtest.profitFactor) }; if (backtest.trades < 30 || paper.trades < 10) return { ...common, status: 'INSUFFICIENT_DATA', reasons: ['São necessários pelo menos 30 trades de backtest e 10 de Paper Trading para comparação robusta.'] }; const reasons: string[] = []; if (Math.abs(common.winRateDeltaPercent) > 15) reasons.push(`Win rate diverge ${round(Math.abs(common.winRateDeltaPercent), 2)} pontos percentuais.`); if (Math.abs(common.expectancyDeltaR) > 0.25) reasons.push(`Expectancy diverge ${round(Math.abs(common.expectancyDeltaR), 3)}R.`); if (Math.abs(common.drawdownDeltaPercent) > 5) reasons.push(`Drawdown máximo diverge ${round(Math.abs(common.drawdownDeltaPercent), 2)} pontos percentuais.`); return { ...common, status: reasons.length ? 'DIVERGENT' : 'ALIGNED', reasons: reasons.length ? reasons : ['Métricas principais estão dentro das bandas de alinhamento definidas.'] }; }
|
| 28 |
-
function assessQuality(result: HistoricalBacktestResult): QuantitativeLabResult['quality'] { if (result.totalTrades < 30) return { score: 0, grade: 'INSUFFICIENT_DATA', checks: [`Amostra: ${result.totalTrades} trades.`], warnings: ['Menos de 30 trades.'] }; let score = 0; const checks: string[] = []; const warnings: string[] = []; if (result.expectancyR > 0) { score += 25; checks.push('Expectancy positiva.'); } else warnings.push('Expectancy não é positiva.'); if (result.profitFactor > 1.2) { score += 20; checks.push('Profit Factor acima de 1.20.'); } else warnings.push('Profit Factor baixo.'); if (result.maxDrawdownPercent < 10) { score += 20; checks.push('Drawdown abaixo de 10%.'); } else warnings.push('Drawdown elevado.'); if (result.winRate >= 45) { score += 15; checks.push('Win rate >= 45%.'); } else warnings.push('Win rate abaixo de 45%.'); if (result.sharpeRatio > 1) { score += 10; checks.push('Sharpe acima de 1.'); } else warnings.push('Sharpe não supera 1.'); if (result.totalTrades >= 100) { score += 10; checks.push('Amostra >= 100 trades.'); } else warnings.push('Amostra ainda abaixo de 100 trades.'); const grade = score >= 85 ? 'A' : score >= 70 ? 'B' : score >= 50 ? 'C' : 'D'; return { score, grade, checks, warnings }; }
|
| 29 |
-
const baseOptions = { initialCapital: 10_000, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220 } as const;
|
| 30 |
-
const emptySelection = (): RobustParameterSelectionResult => ({ candidates: [], selected: null, baseline: null, stableRegion: { minScore: [], minConfidence: [], atrStopMultiple: [], rewardRisk: [], maxHoldingBars: [] }, grade: 'INSUFFICIENT_DATA', warnings: ['Otimização de parâmetros é executada apenas pela validação OOS.'] });
|
| 31 |
-
export function buildQuantitativeLab(result: HistoricalBacktestResult, paperState: PaperTradingState, candles: Candle[]): QuantitativeLabResult { const backtest = backtestMetrics(result); const paper = paperMetrics(paperState); return { generatedAt: Date.now(), symbol: result.symbol, backtest, paper, comparison: buildComparison(backtest, paper), quality: assessQuality(result), regimes: analyzeRegimes(candles, result.trades), statistics: analyzeStatistics(result.trades), robustness: analyzeRobustness(result.trades), timeSeries: analyzeTimeSeries(result.trades), walkForward: runWalkForwardAnalysis(result.symbol, candles, baseOptions), stressTest: runStressTest(result.symbol, candles, baseOptions), monteCarlo: analyzeMonteCarlo(result.trades, 3000, baseOptions.riskPerTradePercent), parameterSelection: emptySelection() }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/regimeAnalytics.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import type { HistoricalBacktestTrade } from '../backtest/historicalBacktest.js';
|
| 3 |
-
|
| 4 |
-
export type MarketRegime = 'TREND_BULL' | 'TREND_BEAR' | 'RANGE' | 'HIGH_VOLATILITY' | 'LOW_VOLATILITY' | 'NEUTRAL';
|
| 5 |
-
export interface RegimeBucket { regime: MarketRegime; trades: number; wins: number; losses: number; winRatePercent: number; netR: number; expectancyR: number; profitFactor: number; maxDrawdownR: number; }
|
| 6 |
-
export interface RegimeAnalyticsResult { buckets: RegimeBucket[]; dominantRegime: MarketRegime; strongestRegime: MarketRegime | null; weakestRegime: MarketRegime | null; warnings: string[]; }
|
| 7 |
-
const mean = (values: number[]) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
| 8 |
-
const round = (value: number, digits = 4) => Number((Number.isFinite(value) ? value : 0).toFixed(digits));
|
| 9 |
-
function classifyRegime(candles: Candle[], index: number): MarketRegime { const start = Math.max(0, index - 49); const window = candles.slice(start, index + 1); if (window.length < 20) return 'NEUTRAL'; const closes = window.map(c => c.close); const returns = closes.slice(1).map((v, i) => v / closes[i] - 1); const meanReturn = mean(returns); const volatility = Math.sqrt(mean(returns.map(r => (r - meanReturn) ** 2))); const move = closes.at(-1)! / closes[0] - 1; const threshold = Math.max(volatility * 2, 0.0015); if (volatility > 0.008) return 'HIGH_VOLATILITY'; if (volatility < 0.0025) return 'LOW_VOLATILITY'; if (move > threshold * 2) return 'TREND_BULL'; if (move < -threshold * 2) return 'TREND_BEAR'; return 'RANGE'; }
|
| 10 |
-
function drawdownR(values: number[]): number { let equity = 0; let peak = 0; let max = 0; for (const value of values) { equity += value; peak = Math.max(peak, equity); max = Math.max(max, peak - equity); } return max; }
|
| 11 |
-
export function analyzeRegimes(candles: Candle[], trades: HistoricalBacktestTrade[]): RegimeAnalyticsResult {
|
| 12 |
-
const buckets = new Map<MarketRegime, number[]>();
|
| 13 |
-
for (const trade of trades) {
|
| 14 |
-
let entryIndex = candles.findIndex(c => c.timestamp === trade.timestamp);
|
| 15 |
-
if (entryIndex < 0) entryIndex = candles.findIndex(c => c.timestamp >= trade.timestamp);
|
| 16 |
-
if (entryIndex <= 0) continue;
|
| 17 |
-
// Trade timestamps represent the next candle's entry. Classify the regime
|
| 18 |
-
// using the signal candle immediately before entry to avoid look-ahead.
|
| 19 |
-
const regime = classifyRegime(candles, entryIndex - 1);
|
| 20 |
-
buckets.set(regime, [...(buckets.get(regime) ?? []), trade.pnlR]);
|
| 21 |
-
}
|
| 22 |
-
const allRegimes: MarketRegime[] = ['TREND_BULL', 'TREND_BEAR', 'RANGE', 'HIGH_VOLATILITY', 'LOW_VOLATILITY', 'NEUTRAL'];
|
| 23 |
-
const result = allRegimes.map(regime => { const values = buckets.get(regime) ?? []; const wins = values.filter(v => v > 0).length; const losses = values.filter(v => v < 0).length; const grossWin = values.filter(v => v > 0).reduce((a, b) => a + b, 0); const grossLoss = Math.abs(values.filter(v => v < 0).reduce((a, b) => a + b, 0)); return { regime, trades: values.length, wins, losses, winRatePercent: round(values.length ? wins / values.length * 100 : 0, 2), netR: round(values.reduce((a, b) => a + b, 0)), expectancyR: round(mean(values)), profitFactor: round(grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0), maxDrawdownR: round(drawdownR(values)) }; });
|
| 24 |
-
const active = result.filter(x => x.trades > 0); const strongest = active.filter(x => x.trades >= 5).sort((a, b) => b.expectancyR - a.expectancyR)[0]?.regime ?? null; const weakest = active.filter(x => x.trades >= 5).sort((a, b) => a.expectancyR - b.expectancyR)[0]?.regime ?? null; const dominant = active.sort((a, b) => b.trades - a.trades)[0]?.regime ?? 'NEUTRAL'; const warnings: string[] = [];
|
| 25 |
-
for (const bucket of active) { if (bucket.trades < 5) warnings.push(`${bucket.regime}: amostra pequena (${bucket.trades} trades).`); if (bucket.expectancyR < 0) warnings.push(`${bucket.regime}: expectancy negativa.`); }
|
| 26 |
-
return { buckets: result, dominantRegime: dominant, strongestRegime: strongest, weakestRegime: weakest, warnings };
|
| 27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/robustParameterSelection.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestOptions, HistoricalBacktestResult } from '../backtest/historicalBacktest.js';
|
| 2 |
-
import type { Candle } from '../../src/types.js';
|
| 3 |
-
import { runHistoricalBacktest } from '../backtest/historicalBacktest.js';
|
| 4 |
-
|
| 5 |
-
export interface ParameterCandidate { id: string; minScore: number; minConfidence: number; atrStopMultiple: number; rewardRisk: number; maxHoldingBars: number; trades: number; expectancyR: number; profitFactor: number; netProfitPercent: number; maxDrawdownPercent: number; stabilityScore: number; rankScore: number; }
|
| 6 |
-
export interface RobustParameterSelectionResult { candidates: ParameterCandidate[]; selected: ParameterCandidate | null; baseline: ParameterCandidate | null; stableRegion: { minScore: number[]; minConfidence: number[]; atrStopMultiple: number[]; rewardRisk: number[]; maxHoldingBars: number[] }; grade: 'ROBUST' | 'PROMISING' | 'FRAGILE' | 'INSUFFICIENT_DATA'; warnings: string[]; }
|
| 7 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 8 |
-
const pf = (r: HistoricalBacktestResult) => Number.isFinite(r.profitFactor) ? r.profitFactor : r.profitFactor > 0 ? 99 : 0;
|
| 9 |
-
function score(r: HistoricalBacktestResult): number { if (r.totalTrades < 20) return 0; const expectancy = Math.max(-1, Math.min(2, r.expectancyR)); const profitFactor = Math.max(0, Math.min(3, pf(r))); const ddPenalty = Math.max(0, Math.min(1, r.maxDrawdownPercent / 20)); const sampleBonus = Math.min(1, r.totalTrades / 100); return round(Math.max(0, expectancy / 2) * 35 + Math.min(1, profitFactor / 2) * 30 + (1 - ddPenalty) * 20 + sampleBonus * 15, 2); }
|
| 10 |
-
function candidate(id: string, options: Omit<HistoricalBacktestOptions, 'symbol' | 'candles'>, r: HistoricalBacktestResult): ParameterCandidate { const rankScore = score(r); return { id, minScore: options.minScore ?? 35, minConfidence: options.minConfidence ?? 50, atrStopMultiple: options.atrStopMultiple ?? 1.5, rewardRisk: options.rewardRisk ?? 2, maxHoldingBars: options.maxHoldingBars ?? 32, trades: r.totalTrades, expectancyR: round(r.expectancyR), profitFactor: round(pf(r)), netProfitPercent: round(r.netProfitPercent, 2), maxDrawdownPercent: round(r.maxDrawdownPercent, 2), stabilityScore: rankScore, rankScore }; }
|
| 11 |
-
function configKey(config: [number, number, number, number, number]) { return config.join('-'); }
|
| 12 |
-
|
| 13 |
-
/** Bounded local search: 15 strategically spaced configurations instead of the previous 31 full backtests. */
|
| 14 |
-
export function selectRobustParameters(symbol: string, candles: Candle[], baseOptions: Omit<HistoricalBacktestOptions, 'symbol' | 'candles'> = {}): RobustParameterSelectionResult {
|
| 15 |
-
const base: [number, number, number, number, number] = [baseOptions.minScore ?? 35, baseOptions.minConfidence ?? 50, baseOptions.atrStopMultiple ?? 1.5, baseOptions.rewardRisk ?? 2, baseOptions.maxHoldingBars ?? 32];
|
| 16 |
-
const configs: Array<[number, number, number, number, number]> = [base];
|
| 17 |
-
const add = (config: [number, number, number, number, number]) => { if (!configs.some(existing => configKey(existing) === configKey(config))) configs.push(config); };
|
| 18 |
-
add([30, base[1], base[2], base[3], base[4]]); add([40, base[1], base[2], base[3], base[4]]);
|
| 19 |
-
add([base[0], 45, base[2], base[3], base[4]]); add([base[0], 55, base[2], base[3], base[4]]);
|
| 20 |
-
add([base[0], base[1], 1.25, base[3], base[4]]); add([base[0], base[1], 1.75, base[3], base[4]]);
|
| 21 |
-
add([base[0], base[1], base[2], 1.75, base[4]]); add([base[0], base[1], base[2], 2.25, base[4]]);
|
| 22 |
-
add([base[0], base[1], base[2], base[3], 24]); add([base[0], base[1], base[2], base[3], 40]);
|
| 23 |
-
add([30, 45, 1.25, 1.75, 24]); add([40, 55, 1.75, 2.25, 40]); add([30, 55, 1.75, 2.25, 32]); add([40, 45, 1.25, 2, 40]);
|
| 24 |
-
|
| 25 |
-
const candidates: ParameterCandidate[] = [];
|
| 26 |
-
for (const [s, c, a, r, h] of configs) {
|
| 27 |
-
const options = { ...baseOptions, minScore: s, minConfidence: c, atrStopMultiple: a, rewardRisk: r, maxHoldingBars: h };
|
| 28 |
-
candidates.push(candidate(`${s}-${c}-${a}-${r}-${h}`, options, runHistoricalBacktest({ ...options, symbol, candles })));
|
| 29 |
-
}
|
| 30 |
-
const baseline = candidates.find(c => c.minScore === base[0] && c.minConfidence === base[1] && c.atrStopMultiple === base[2] && c.rewardRisk === base[3] && c.maxHoldingBars === base[4]) ?? null;
|
| 31 |
-
for (const c of candidates) {
|
| 32 |
-
const nearby = candidates.filter(n => { const distance = Math.abs(n.minScore - c.minScore) / 10 + Math.abs(n.minConfidence - c.minConfidence) / 10 + Math.abs(n.atrStopMultiple - c.atrStopMultiple) / 0.25 + Math.abs(n.rewardRisk - c.rewardRisk) / 0.25 + Math.abs(n.maxHoldingBars - c.maxHoldingBars) / 8; return distance <= 2.01 && n.trades >= 30; });
|
| 33 |
-
const viable = nearby.filter(n => n.expectancyR > 0 && n.profitFactor > 1).length;
|
| 34 |
-
c.stabilityScore = round(c.rankScore * 0.7 + (nearby.length ? viable / nearby.length * 100 : 0) * 0.3, 2);
|
| 35 |
-
}
|
| 36 |
-
const eligible = candidates.filter(c => c.trades >= 30 && c.expectancyR > 0 && c.profitFactor > 1);
|
| 37 |
-
eligible.sort((a, b) => b.stabilityScore - a.stabilityScore || b.rankScore - a.rankScore || b.trades - a.trades);
|
| 38 |
-
const selected = eligible[0] ?? null;
|
| 39 |
-
const stable = candidates.filter(c => selected && Math.abs(c.stabilityScore - selected.stabilityScore) <= 8 && c.trades >= 30 && c.expectancyR > 0 && c.profitFactor > 1);
|
| 40 |
-
const warnings: string[] = [];
|
| 41 |
-
if (selected && baseline && selected.id !== baseline.id) warnings.push('A configuração selecionada difere do baseline. Validar em OOS antes de qualquer uso operacional.');
|
| 42 |
-
if (candles.length < 1500) warnings.push('Histórico curto para otimização robusta; aumentar a janela antes de concluir sobre parâmetros.');
|
| 43 |
-
if (!selected) warnings.push('Nenhuma configuração atingiu os critérios mínimos de robustez.');
|
| 44 |
-
warnings.push('Busca compacta de parâmetros: validação OOS e estabilidade regional continuam obrigatórias.');
|
| 45 |
-
const grade = selected === null ? 'INSUFFICIENT_DATA' : stable.length >= 7 ? 'ROBUST' : stable.length >= 3 ? 'PROMISING' : 'FRAGILE';
|
| 46 |
-
return { candidates: candidates.sort((a, b) => b.stabilityScore - a.stabilityScore).slice(0, 30), selected, baseline, stableRegion: { minScore: [...new Set(stable.map(c => c.minScore))].sort((a, b) => a - b), minConfidence: [...new Set(stable.map(c => c.minConfidence))].sort((a, b) => a - b), atrStopMultiple: [...new Set(stable.map(c => c.atrStopMultiple))].sort((a, b) => a - b), rewardRisk: [...new Set(stable.map(c => c.rewardRisk))].sort((a, b) => a - b), maxHoldingBars: [...new Set(stable.map(c => c.maxHoldingBars))].sort((a, b) => a - b) }, grade, warnings };
|
| 47 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/robustnessAnalysis.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestTrade } from '../backtest/historicalBacktest.js';
|
| 2 |
-
|
| 3 |
-
export interface RobustnessAnalysisResult {
|
| 4 |
-
sampleSize: number;
|
| 5 |
-
positiveExpectancy: boolean;
|
| 6 |
-
profitFactorAboveOne: boolean;
|
| 7 |
-
bootstrapExpectancy95: { low: number; high: number } | null;
|
| 8 |
-
stabilityScore: number;
|
| 9 |
-
grade: 'ROBUST' | 'MODERATE' | 'FRAGILE' | 'INSUFFICIENT_DATA';
|
| 10 |
-
checks: string[];
|
| 11 |
-
warnings: string[];
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 15 |
-
|
| 16 |
-
function mean(values: number[]): number {
|
| 17 |
-
return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
function seededRandom(seed: number): () => number {
|
| 21 |
-
let state = seed >>> 0;
|
| 22 |
-
return () => {
|
| 23 |
-
state = (1664525 * state + 1013904223) >>> 0;
|
| 24 |
-
return state / 4294967296;
|
| 25 |
-
};
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
function bootstrap(values: number[], iterations = 2000, seed = 20260908): { low: number; high: number } | null {
|
| 29 |
-
if (values.length < 30) return null;
|
| 30 |
-
const random = seededRandom(seed + values.length);
|
| 31 |
-
const samples = new Array<number>(iterations);
|
| 32 |
-
for (let i = 0; i < iterations; i++) {
|
| 33 |
-
let total = 0;
|
| 34 |
-
for (let j = 0; j < values.length; j++) total += values[Math.floor(random() * values.length)];
|
| 35 |
-
samples[i] = total / values.length;
|
| 36 |
-
}
|
| 37 |
-
samples.sort((a, b) => a - b);
|
| 38 |
-
return { low: samples[Math.floor(iterations * 0.025)], high: samples[Math.floor(iterations * 0.975)] };
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
export function analyzeRobustness(trades: HistoricalBacktestTrade[]): RobustnessAnalysisResult {
|
| 42 |
-
const values = trades.map(t => t.pnlR).filter(Number.isFinite);
|
| 43 |
-
if (values.length < 30) {
|
| 44 |
-
return {
|
| 45 |
-
sampleSize: values.length,
|
| 46 |
-
positiveExpectancy: mean(values) > 0,
|
| 47 |
-
profitFactorAboveOne: false,
|
| 48 |
-
bootstrapExpectancy95: null,
|
| 49 |
-
stabilityScore: 0,
|
| 50 |
-
grade: 'INSUFFICIENT_DATA',
|
| 51 |
-
checks: [],
|
| 52 |
-
warnings: ['São necessários pelo menos 30 trades para a análise de robustez.'],
|
| 53 |
-
};
|
| 54 |
-
}
|
| 55 |
-
const wins = values.filter(v => v > 0).reduce((a, b) => a + b, 0);
|
| 56 |
-
const losses = Math.abs(values.filter(v => v < 0).reduce((a, b) => a + b, 0));
|
| 57 |
-
const pf = losses > 0 ? wins / losses : Infinity;
|
| 58 |
-
const expectancy = mean(values);
|
| 59 |
-
const bootstrapCi = bootstrap(values);
|
| 60 |
-
const checks: string[] = [];
|
| 61 |
-
const warnings: string[] = [];
|
| 62 |
-
let score = 0;
|
| 63 |
-
if (expectancy > 0) { score += 25; checks.push('Expectancy histórica positiva.'); } else warnings.push('Expectancy histórica negativa.');
|
| 64 |
-
if (pf > 1.2) { score += 25; checks.push('Profit Factor acima de 1.20.'); } else warnings.push('Profit Factor não supera 1.20.');
|
| 65 |
-
if (bootstrapCi && bootstrapCi.low > 0) { score += 30; checks.push('Bootstrap 95% da expectancy permanece acima de zero.'); } else warnings.push('Bootstrap 95% ainda inclui expectancy não positiva.');
|
| 66 |
-
const median = [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)];
|
| 67 |
-
if (Math.sign(expectancy) === Math.sign(median) && median > 0) { score += 10; checks.push('Mediana e média possuem sinal positivo.'); } else warnings.push('Distribuição não confirma claramente a vantagem pela mediana.');
|
| 68 |
-
const firstHalf = mean(values.slice(0, Math.floor(values.length / 2)));
|
| 69 |
-
const secondHalf = mean(values.slice(Math.floor(values.length / 2)));
|
| 70 |
-
if (firstHalf > 0 && secondHalf > 0) { score += 10; checks.push('Expectancy positiva nas duas metades da amostra.'); } else warnings.push('Uma das metades da amostra apresenta expectancy não positiva.');
|
| 71 |
-
const grade = score >= 80 ? 'ROBUST' : score >= 55 ? 'MODERATE' : 'FRAGILE';
|
| 72 |
-
return {
|
| 73 |
-
sampleSize: values.length,
|
| 74 |
-
positiveExpectancy: expectancy > 0,
|
| 75 |
-
profitFactorAboveOne: pf > 1,
|
| 76 |
-
bootstrapExpectancy95: bootstrapCi ? { low: round(bootstrapCi.low), high: round(bootstrapCi.high) } : null,
|
| 77 |
-
stabilityScore: score,
|
| 78 |
-
grade,
|
| 79 |
-
checks,
|
| 80 |
-
warnings,
|
| 81 |
-
};
|
| 82 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/statisticalAnalysis.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestTrade } from '../backtest/historicalBacktest.js';
|
| 2 |
-
|
| 3 |
-
export interface DirectionBucket {
|
| 4 |
-
direction: 'LONG' | 'SHORT' | 'UNKNOWN';
|
| 5 |
-
trades: number;
|
| 6 |
-
wins: number;
|
| 7 |
-
losses: number;
|
| 8 |
-
winRatePercent: number;
|
| 9 |
-
netR: number;
|
| 10 |
-
expectancyR: number;
|
| 11 |
-
medianR: number;
|
| 12 |
-
stdDevR: number;
|
| 13 |
-
profitFactor: number;
|
| 14 |
-
payoffRatio: number;
|
| 15 |
-
maxConsecutiveLosses: number;
|
| 16 |
-
maxConsecutiveWins: number;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
export interface StatisticalAnalysisResult {
|
| 20 |
-
sampleSize: number;
|
| 21 |
-
meanR: number;
|
| 22 |
-
medianR: number;
|
| 23 |
-
stdDevR: number;
|
| 24 |
-
standardErrorR: number;
|
| 25 |
-
expectancyCi95R: { low: number; high: number } | null;
|
| 26 |
-
positiveTradeRatePercent: number;
|
| 27 |
-
payoffRatio: number;
|
| 28 |
-
maxConsecutiveLosses: number;
|
| 29 |
-
maxConsecutiveWins: number;
|
| 30 |
-
directions: DirectionBucket[];
|
| 31 |
-
warnings: string[];
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 35 |
-
|
| 36 |
-
function median(values: number[]): number {
|
| 37 |
-
if (!values.length) return 0;
|
| 38 |
-
const sorted = [...values].sort((a, b) => a - b);
|
| 39 |
-
const mid = Math.floor(sorted.length / 2);
|
| 40 |
-
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
function stats(values: number[]) {
|
| 44 |
-
const mean = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
| 45 |
-
const variance = values.length > 1
|
| 46 |
-
? values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1)
|
| 47 |
-
: 0;
|
| 48 |
-
const wins = values.filter(v => v > 0);
|
| 49 |
-
const losses = values.filter(v => v < 0);
|
| 50 |
-
const grossWin = wins.reduce((a, b) => a + b, 0);
|
| 51 |
-
const grossLoss = Math.abs(losses.reduce((a, b) => a + b, 0));
|
| 52 |
-
return {
|
| 53 |
-
mean,
|
| 54 |
-
median: median(values),
|
| 55 |
-
stdDev: Math.sqrt(variance),
|
| 56 |
-
wins: wins.length,
|
| 57 |
-
losses: losses.length,
|
| 58 |
-
profitFactor: grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0,
|
| 59 |
-
payoffRatio: wins.length && losses.length ? (grossWin / wins.length) / (grossLoss / losses.length) : 0,
|
| 60 |
-
};
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
function streak(values: number[]) {
|
| 64 |
-
let wins = 0, losses = 0, maxWins = 0, maxLosses = 0;
|
| 65 |
-
for (const value of values) {
|
| 66 |
-
if (value > 0) { wins++; losses = 0; maxWins = Math.max(maxWins, wins); }
|
| 67 |
-
else if (value < 0) { losses++; wins = 0; maxLosses = Math.max(maxLosses, losses); }
|
| 68 |
-
else { wins = 0; losses = 0; }
|
| 69 |
-
}
|
| 70 |
-
return { maxWins, maxLosses };
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
function directionOf(trade: HistoricalBacktestTrade): DirectionBucket['direction'] {
|
| 74 |
-
const candidate = trade as HistoricalBacktestTrade & { direction?: string; side?: string; signal?: { direction?: string } };
|
| 75 |
-
const value = String(candidate.direction ?? candidate.side ?? candidate.signal?.direction ?? '').toUpperCase();
|
| 76 |
-
if (value.includes('LONG') || value === 'BUY') return 'LONG';
|
| 77 |
-
if (value.includes('SHORT') || value === 'SELL') return 'SHORT';
|
| 78 |
-
return 'UNKNOWN';
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
function directionBucket(direction: DirectionBucket['direction'], trades: HistoricalBacktestTrade[]): DirectionBucket {
|
| 82 |
-
const values = trades.map(t => t.pnlR);
|
| 83 |
-
const s = stats(values);
|
| 84 |
-
const streaks = streak(values);
|
| 85 |
-
return {
|
| 86 |
-
direction,
|
| 87 |
-
trades: values.length,
|
| 88 |
-
wins: s.wins,
|
| 89 |
-
losses: s.losses,
|
| 90 |
-
winRatePercent: round(values.length ? s.wins / values.length * 100 : 0, 2),
|
| 91 |
-
netR: round(values.reduce((a, b) => a + b, 0)),
|
| 92 |
-
expectancyR: round(s.mean),
|
| 93 |
-
medianR: round(s.median),
|
| 94 |
-
stdDevR: round(s.stdDev),
|
| 95 |
-
profitFactor: round(s.profitFactor),
|
| 96 |
-
payoffRatio: round(s.payoffRatio),
|
| 97 |
-
maxConsecutiveLosses: streaks.maxLosses,
|
| 98 |
-
maxConsecutiveWins: streaks.maxWins,
|
| 99 |
-
};
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
export function analyzeStatistics(trades: HistoricalBacktestTrade[]): StatisticalAnalysisResult {
|
| 103 |
-
const values = trades.map(t => t.pnlR).filter(Number.isFinite);
|
| 104 |
-
const s = stats(values);
|
| 105 |
-
const streaks = streak(values);
|
| 106 |
-
const standardError = values.length > 1 ? s.stdDev / Math.sqrt(values.length) : 0;
|
| 107 |
-
const ci = values.length >= 30 ? { low: s.mean - 1.96 * standardError, high: s.mean + 1.96 * standardError } : null;
|
| 108 |
-
const groups = new Map<DirectionBucket['direction'], HistoricalBacktestTrade[]>();
|
| 109 |
-
for (const trade of trades) {
|
| 110 |
-
const direction = directionOf(trade);
|
| 111 |
-
groups.set(direction, [...(groups.get(direction) ?? []), trade]);
|
| 112 |
-
}
|
| 113 |
-
const directions = (['LONG', 'SHORT', 'UNKNOWN'] as const).map(direction => directionBucket(direction, groups.get(direction) ?? []));
|
| 114 |
-
const warnings: string[] = [];
|
| 115 |
-
if (values.length < 30) warnings.push(`Amostra estatística pequena: ${values.length} trades.`);
|
| 116 |
-
if (values.length >= 30 && s.mean <= 0) warnings.push('Intervalo de confiança da expectancy não parte de uma média positiva.');
|
| 117 |
-
if (s.stdDev > Math.abs(s.mean) * 3 && values.length >= 30) warnings.push('Alta dispersão dos resultados em relação à expectancy.');
|
| 118 |
-
if (streaks.maxLosses >= 6) warnings.push(`Sequência máxima de ${streaks.maxLosses} perdas consecutivas.`);
|
| 119 |
-
for (const bucket of directions.filter(d => d.trades > 0)) {
|
| 120 |
-
if (bucket.trades < 20) warnings.push(`${bucket.direction}: amostra abaixo de 20 trades.`);
|
| 121 |
-
if (bucket.expectancyR < 0) warnings.push(`${bucket.direction}: expectancy negativa.`);
|
| 122 |
-
}
|
| 123 |
-
return {
|
| 124 |
-
sampleSize: values.length,
|
| 125 |
-
meanR: round(s.mean),
|
| 126 |
-
medianR: round(s.median),
|
| 127 |
-
stdDevR: round(s.stdDev),
|
| 128 |
-
standardErrorR: round(standardError),
|
| 129 |
-
expectancyCi95R: ci ? { low: round(ci.low), high: round(ci.high) } : null,
|
| 130 |
-
positiveTradeRatePercent: round(values.length ? s.wins / values.length * 100 : 0, 2),
|
| 131 |
-
payoffRatio: round(s.payoffRatio),
|
| 132 |
-
maxConsecutiveLosses: streaks.maxLosses,
|
| 133 |
-
maxConsecutiveWins: streaks.maxWins,
|
| 134 |
-
directions,
|
| 135 |
-
warnings,
|
| 136 |
-
};
|
| 137 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/stressTest.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestOptions, HistoricalBacktestResult } from '../backtest/historicalBacktest.js';
|
| 2 |
-
import { runHistoricalBacktest } from '../backtest/historicalBacktest.js';
|
| 3 |
-
import type { Candle } from '../../src/types.js';
|
| 4 |
-
|
| 5 |
-
export interface StressScenario {
|
| 6 |
-
name: string;
|
| 7 |
-
feeBpsPerSide: number;
|
| 8 |
-
slippageBpsPerSide: number;
|
| 9 |
-
latencySlippageBpsPerSide: number;
|
| 10 |
-
fundingRatePer8h: number;
|
| 11 |
-
result: { trades: number; netProfitPercent: number; expectancyR: number; profitFactor: number; maxDrawdownPercent: number; winRatePercent: number };
|
| 12 |
-
deltaFromBase: { netProfitPercent: number; expectancyR: number; maxDrawdownPercent: number };
|
| 13 |
-
status: 'PASS' | 'DEGRADED' | 'FAIL' | 'INSUFFICIENT_DATA';
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export interface StressTestResult {
|
| 17 |
-
base: StressScenario;
|
| 18 |
-
scenarios: StressScenario[];
|
| 19 |
-
passedScenarios: number;
|
| 20 |
-
grade: 'RESILIENT' | 'MODERATE' | 'FRAGILE' | 'INSUFFICIENT_DATA';
|
| 21 |
-
warnings: string[];
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 25 |
-
const displayProfitFactor = (v: number) => Number.isFinite(v) ? round(v) : v > 0 ? 99 : 0;
|
| 26 |
-
|
| 27 |
-
function summarize(result: HistoricalBacktestResult, fee: number, slippage: number, latency: number, funding: number, base?: HistoricalBacktestResult): StressScenario {
|
| 28 |
-
const netDelta = base ? result.netProfitPercent - base.netProfitPercent : 0;
|
| 29 |
-
const expDelta = base ? result.expectancyR - base.expectancyR : 0;
|
| 30 |
-
const ddDelta = base ? result.maxDrawdownPercent - base.maxDrawdownPercent : 0;
|
| 31 |
-
const enough = result.totalTrades >= 30;
|
| 32 |
-
const positive = result.expectancyR > 0 && result.profitFactor > 1;
|
| 33 |
-
const baseExpectancy = base?.expectancyR ?? result.expectancyR;
|
| 34 |
-
const expectancyThreshold = baseExpectancy > 0 ? baseExpectancy * 0.7 : 0;
|
| 35 |
-
const status = !enough ? 'INSUFFICIENT_DATA' : !positive ? 'FAIL' : result.expectancyR >= expectancyThreshold ? 'PASS' : 'DEGRADED';
|
| 36 |
-
return { name: `${fee}bps fee / ${slippage}bps slip / ${latency}bps latency / ${(funding * 100).toFixed(3)}% funding/8h`, feeBpsPerSide: fee, slippageBpsPerSide: slippage, latencySlippageBpsPerSide: latency, fundingRatePer8h: funding, result: { trades: result.totalTrades, netProfitPercent: round(result.netProfitPercent, 2), expectancyR: round(result.expectancyR), profitFactor: displayProfitFactor(result.profitFactor), maxDrawdownPercent: round(result.maxDrawdownPercent, 2), winRatePercent: round(result.winRate, 2) }, deltaFromBase: { netProfitPercent: round(netDelta, 2), expectancyR: round(expDelta), maxDrawdownPercent: round(ddDelta, 2) }, status };
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
export function runStressTest(symbol: string, candles: Candle[], baseOptions: Omit<HistoricalBacktestOptions, 'symbol' | 'candles' | 'feeBpsPerSide' | 'slippageBpsPerSide' | 'latencySlippageBpsPerSide' | 'fundingRatePer8h'> = {}): StressTestResult {
|
| 40 |
-
const baseResult = runHistoricalBacktest({ ...baseOptions, symbol, candles, feeBpsPerSide: 5, slippageBpsPerSide: 2, latencySlippageBpsPerSide: 1, fundingRatePer8h: 0.0001 });
|
| 41 |
-
const scenarios = [
|
| 42 |
-
[7, 3, 1, 0.0001],
|
| 43 |
-
[10, 5, 2, 0.0002],
|
| 44 |
-
[15, 8, 3, 0.0003],
|
| 45 |
-
[20, 10, 5, 0.0005],
|
| 46 |
-
].map(([fee, slippage, latency, funding]) => summarize(
|
| 47 |
-
runHistoricalBacktest({ ...baseOptions, symbol, candles, feeBpsPerSide: fee, slippageBpsPerSide: slippage, latencySlippageBpsPerSide: latency, fundingRatePer8h: funding }),
|
| 48 |
-
fee, slippage, latency, funding, baseResult,
|
| 49 |
-
));
|
| 50 |
-
const base = summarize(baseResult, 5, 2, 1, 0.0001);
|
| 51 |
-
const passed = scenarios.filter(s => s.status === 'PASS').length;
|
| 52 |
-
const evaluated = scenarios.filter(s => s.status !== 'INSUFFICIENT_DATA').length;
|
| 53 |
-
const warnings: string[] = [];
|
| 54 |
-
if (base.result.trades < 30) warnings.push('Amostra base inferior a 30 trades.');
|
| 55 |
-
if (evaluated < scenarios.length) warnings.push('Alguns cenários não possuem amostra suficiente.');
|
| 56 |
-
if (scenarios.some(s => s.status === 'FAIL')) warnings.push('A estratégia perde expectancy positiva em pelo menos um cenário de custos.');
|
| 57 |
-
if (scenarios.some(s => s.result.maxDrawdownPercent > 15)) warnings.push('Drawdown acima de 15% em cenário de stress.');
|
| 58 |
-
const grade = base.result.trades < 30 ? 'INSUFFICIENT_DATA' : passed === scenarios.length ? 'RESILIENT' : passed >= 2 ? 'MODERATE' : 'FRAGILE';
|
| 59 |
-
return { base, scenarios, passedScenarios: passed, grade, warnings };
|
| 60 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/timeSeriesAnalysis.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestTrade } from '../backtest/historicalBacktest.js';
|
| 2 |
-
|
| 3 |
-
export interface TimeBucket {
|
| 4 |
-
period: string;
|
| 5 |
-
trades: number;
|
| 6 |
-
netR: number;
|
| 7 |
-
expectancyR: number;
|
| 8 |
-
winRatePercent: number;
|
| 9 |
-
profitFactor: number;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
export interface TimeSeriesAnalysisResult {
|
| 13 |
-
periods: TimeBucket[];
|
| 14 |
-
positivePeriods: number;
|
| 15 |
-
negativePeriods: number;
|
| 16 |
-
consistencyPercent: number;
|
| 17 |
-
bestPeriod: string | null;
|
| 18 |
-
worstPeriod: string | null;
|
| 19 |
-
warnings: string[];
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 23 |
-
|
| 24 |
-
function periodKey(timestamp: number): string {
|
| 25 |
-
const date = new Date(timestamp);
|
| 26 |
-
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export function analyzeTimeSeries(trades: HistoricalBacktestTrade[]): TimeSeriesAnalysisResult {
|
| 30 |
-
const groups = new Map<string, HistoricalBacktestTrade[]>();
|
| 31 |
-
for (const trade of trades) {
|
| 32 |
-
const key = periodKey(trade.timestamp);
|
| 33 |
-
groups.set(key, [...(groups.get(key) ?? []), trade]);
|
| 34 |
-
}
|
| 35 |
-
const periods = [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([period, items]) => {
|
| 36 |
-
const values = items.map(t => t.pnlR).filter(Number.isFinite);
|
| 37 |
-
const wins = values.filter(v => v > 0).reduce((a, b) => a + b, 0);
|
| 38 |
-
const losses = Math.abs(values.filter(v => v < 0).reduce((a, b) => a + b, 0));
|
| 39 |
-
return {
|
| 40 |
-
period,
|
| 41 |
-
trades: values.length,
|
| 42 |
-
netR: round(values.reduce((a, b) => a + b, 0)),
|
| 43 |
-
expectancyR: round(values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0),
|
| 44 |
-
winRatePercent: round(values.length ? values.filter(v => v > 0).length / values.length * 100 : 0, 2),
|
| 45 |
-
profitFactor: round(losses > 0 ? wins / losses : wins > 0 ? Infinity : 0),
|
| 46 |
-
};
|
| 47 |
-
});
|
| 48 |
-
const active = periods.filter(p => p.trades > 0);
|
| 49 |
-
const positive = active.filter(p => p.netR > 0).length;
|
| 50 |
-
const negative = active.filter(p => p.netR < 0).length;
|
| 51 |
-
const warnings: string[] = [];
|
| 52 |
-
if (active.length < 3) warnings.push('Menos de três períodos disponíveis para avaliar estabilidade temporal.');
|
| 53 |
-
for (const period of active) if (period.trades < 10) warnings.push(`${period.period}: amostra pequena (${period.trades} trades).`);
|
| 54 |
-
if (active.length && positive / active.length < 0.5) warnings.push('Menos da metade dos períodos apresentou resultado líquido positivo.');
|
| 55 |
-
return {
|
| 56 |
-
periods,
|
| 57 |
-
positivePeriods: positive,
|
| 58 |
-
negativePeriods: negative,
|
| 59 |
-
consistencyPercent: round(active.length ? positive / active.length * 100 : 0, 2),
|
| 60 |
-
bestPeriod: active.length ? [...active].sort((a, b) => b.expectancyR - a.expectancyR)[0].period : null,
|
| 61 |
-
worstPeriod: active.length ? [...active].sort((a, b) => a.expectancyR - b.expectancyR)[0].period : null,
|
| 62 |
-
warnings,
|
| 63 |
-
};
|
| 64 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/quant/walkForwardAnalysis.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 1 |
-
import type { HistoricalBacktestOptions, HistoricalBacktestResult } from '../backtest/historicalBacktest.js';
|
| 2 |
-
import { runHistoricalBacktest } from '../backtest/historicalBacktest.js';
|
| 3 |
-
import { selectRobustParameters } from './robustParameterSelection.js';
|
| 4 |
-
import type { Candle } from '../../src/types.js';
|
| 5 |
-
|
| 6 |
-
export interface WalkForwardWindow {
|
| 7 |
-
index: number;
|
| 8 |
-
trainStart: string;
|
| 9 |
-
trainEnd: string;
|
| 10 |
-
testStart: string;
|
| 11 |
-
testEnd: string;
|
| 12 |
-
trainTrades: number;
|
| 13 |
-
testTrades: number;
|
| 14 |
-
trainExpectancyR: number;
|
| 15 |
-
testExpectancyR: number;
|
| 16 |
-
trainNetProfitPercent: number;
|
| 17 |
-
testNetProfitPercent: number;
|
| 18 |
-
testProfitFactor: number;
|
| 19 |
-
testMaxDrawdownPercent: number;
|
| 20 |
-
selectedParameters: { minScore: number; minConfidence: number; atrStopMultiple: number; rewardRisk: number; maxHoldingBars: number } | null;
|
| 21 |
-
status: 'PASS' | 'FAIL' | 'INSUFFICIENT_DATA';
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
export interface WalkForwardAnalysisResult {
|
| 25 |
-
windows: WalkForwardWindow[];
|
| 26 |
-
trainBars: number;
|
| 27 |
-
testBars: number;
|
| 28 |
-
stepBars: number;
|
| 29 |
-
adaptive: boolean;
|
| 30 |
-
outOfSampleTrades: number;
|
| 31 |
-
outOfSampleExpectancyR: number;
|
| 32 |
-
outOfSampleNetProfitPercent: number;
|
| 33 |
-
outOfSampleWinRatePercent: number;
|
| 34 |
-
outOfSampleProfitFactor: number;
|
| 35 |
-
outOfSampleMaxDrawdownPercent: number;
|
| 36 |
-
passedWindows: number;
|
| 37 |
-
consistencyPercent: number;
|
| 38 |
-
grade: 'ROBUST' | 'PROMISING' | 'FRAGILE' | 'INSUFFICIENT_DATA';
|
| 39 |
-
warnings: string[];
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
const round = (v: number, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d));
|
| 43 |
-
type BacktestParams = Omit<HistoricalBacktestOptions, 'symbol' | 'candles'>;
|
| 44 |
-
|
| 45 |
-
function runWindow(symbol: string, candles: Candle[], base: BacktestParams): HistoricalBacktestResult {
|
| 46 |
-
return runHistoricalBacktest({ ...base, symbol, candles });
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
function calculateOosMetrics(trades: HistoricalBacktestResult['trades'], riskPerTradePercent: number) {
|
| 50 |
-
let equity = 100;
|
| 51 |
-
let peak = equity;
|
| 52 |
-
let maxDrawdownPercent = 0;
|
| 53 |
-
const riskFraction = Math.max(0.0001, riskPerTradePercent) / 100;
|
| 54 |
-
for (const trade of trades) {
|
| 55 |
-
equity *= Math.max(0, 1 + trade.pnlR * riskFraction);
|
| 56 |
-
peak = Math.max(peak, equity);
|
| 57 |
-
if (peak > 0) maxDrawdownPercent = Math.max(maxDrawdownPercent, (peak - equity) / peak * 100);
|
| 58 |
-
}
|
| 59 |
-
const wins = trades.filter(t => t.pnlR > 0).reduce((a, t) => a + t.pnlR, 0);
|
| 60 |
-
const losses = Math.abs(trades.filter(t => t.pnlR < 0).reduce((a, t) => a + t.pnlR, 0));
|
| 61 |
-
return { netProfitPercent: equity - 100, maxDrawdownPercent, profitFactor: losses > 0 ? wins / losses : wins > 0 ? Infinity : 0 };
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
/** Adaptive rolling walk-forward: each train window selects robust parameters and only then applies them to the following test window. */
|
| 65 |
-
export function runWalkForwardAnalysis(symbol: string, candles: Candle[], baseOptions: BacktestParams = {}, trainBars = 2_000, testBars = 500, stepBars = 500): WalkForwardAnalysisResult {
|
| 66 |
-
const sorted = [...candles].sort((a, b) => a.timestamp - b.timestamp);
|
| 67 |
-
const windows: WalkForwardWindow[] = [];
|
| 68 |
-
const allTestTrades: HistoricalBacktestResult['trades'] = [];
|
| 69 |
-
let windowIndex = 0;
|
| 70 |
-
|
| 71 |
-
for (let trainStart = 0; trainStart + trainBars + testBars <= sorted.length; trainStart += stepBars) {
|
| 72 |
-
const train = sorted.slice(trainStart, trainStart + trainBars);
|
| 73 |
-
const test = sorted.slice(trainStart + trainBars, trainStart + trainBars + testBars);
|
| 74 |
-
const selection = selectRobustParameters(symbol, train, baseOptions);
|
| 75 |
-
const selected = selection.selected;
|
| 76 |
-
const trainResult = selected
|
| 77 |
-
? runWindow(symbol, train, { ...baseOptions, minScore: selected.minScore, minConfidence: selected.minConfidence, atrStopMultiple: selected.atrStopMultiple, rewardRisk: selected.rewardRisk, maxHoldingBars: selected.maxHoldingBars })
|
| 78 |
-
: runWindow(symbol, train, baseOptions);
|
| 79 |
-
const testResult = selected
|
| 80 |
-
? runWindow(symbol, test, { ...baseOptions, minScore: selected.minScore, minConfidence: selected.minConfidence, atrStopMultiple: selected.atrStopMultiple, rewardRisk: selected.rewardRisk, maxHoldingBars: selected.maxHoldingBars })
|
| 81 |
-
: runWindow(symbol, test, baseOptions);
|
| 82 |
-
const enough = !!selected && trainResult.totalTrades >= 20 && testResult.totalTrades >= 10;
|
| 83 |
-
const pass = enough && testResult.expectancyR > 0 && testResult.profitFactor > 1;
|
| 84 |
-
windows.push({
|
| 85 |
-
index: windowIndex++, trainStart: trainResult.startDate, trainEnd: trainResult.endDate, testStart: testResult.startDate, testEnd: testResult.endDate,
|
| 86 |
-
trainTrades: trainResult.totalTrades, testTrades: testResult.totalTrades, trainExpectancyR: round(trainResult.expectancyR), testExpectancyR: round(testResult.expectancyR),
|
| 87 |
-
trainNetProfitPercent: round(trainResult.netProfitPercent, 2), testNetProfitPercent: round(testResult.netProfitPercent, 2), testProfitFactor: round(testResult.profitFactor),
|
| 88 |
-
testMaxDrawdownPercent: round(testResult.maxDrawdownPercent, 2),
|
| 89 |
-
selectedParameters: selected ? { minScore: selected.minScore, minConfidence: selected.minConfidence, atrStopMultiple: selected.atrStopMultiple, rewardRisk: selected.rewardRisk, maxHoldingBars: selected.maxHoldingBars } : null,
|
| 90 |
-
status: !enough ? 'INSUFFICIENT_DATA' : pass ? 'PASS' : 'FAIL',
|
| 91 |
-
});
|
| 92 |
-
allTestTrades.push(...testResult.trades);
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
const wins = allTestTrades.filter(t => t.pnlR > 0);
|
| 96 |
-
const losses = allTestTrades.filter(t => t.pnlR < 0);
|
| 97 |
-
const grossWin = wins.reduce((a, t) => a + t.pnlR, 0);
|
| 98 |
-
const grossLoss = Math.abs(losses.reduce((a, t) => a + t.pnlR, 0));
|
| 99 |
-
const oosExpectancy = allTestTrades.length ? allTestTrades.reduce((a, t) => a + t.pnlR, 0) / allTestTrades.length : 0;
|
| 100 |
-
const passed = windows.filter(w => w.status === 'PASS').length;
|
| 101 |
-
const evaluated = windows.filter(w => w.status !== 'INSUFFICIENT_DATA').length;
|
| 102 |
-
const oosMetrics = calculateOosMetrics(allTestTrades, baseOptions.riskPerTradePercent ?? 1);
|
| 103 |
-
const consistency = evaluated ? passed / evaluated * 100 : 0;
|
| 104 |
-
const warnings: string[] = [];
|
| 105 |
-
if (windows.length < 3) warnings.push('Menos de três janelas walk-forward disponíveis.');
|
| 106 |
-
if (allTestTrades.length < 30) warnings.push(`Amostra OOS pequena: ${allTestTrades.length} trades.`);
|
| 107 |
-
if (evaluated && consistency < 60) warnings.push('Menos de 60% das janelas adaptativas foram positivas.');
|
| 108 |
-
if (oosExpectancy <= 0) warnings.push('Expectancy agregada OOS não é positiva.');
|
| 109 |
-
if (windows.some(w => w.selectedParameters === null)) warnings.push('Uma ou mais janelas não encontraram parâmetros robustos no treino.');
|
| 110 |
-
const grade = allTestTrades.length < 30 || evaluated < 3 ? 'INSUFFICIENT_DATA' : consistency >= 75 && oosExpectancy > 0 ? 'ROBUST' : consistency >= 60 && oosExpectancy > 0 ? 'PROMISING' : 'FRAGILE';
|
| 111 |
-
|
| 112 |
-
return { windows, trainBars, testBars, stepBars, adaptive: true, outOfSampleTrades: allTestTrades.length, outOfSampleExpectancyR: round(oosExpectancy), outOfSampleNetProfitPercent: round(oosMetrics.netProfitPercent, 2), outOfSampleWinRatePercent: round(allTestTrades.length ? wins.length / allTestTrades.length * 100 : 0, 2), outOfSampleProfitFactor: round(grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0), outOfSampleMaxDrawdownPercent: round(oosMetrics.maxDrawdownPercent, 2), passedWindows: passed, consistencyPercent: round(consistency, 2), grade, warnings };
|
| 113 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/realMarketData.ts
DELETED
|
@@ -1,1018 +0,0 @@
|
|
| 1 |
-
import {
|
| 2 |
-
TradeSignal,
|
| 3 |
-
Candle,
|
| 4 |
-
SignalDecision,
|
| 5 |
-
WyckoffPhase,
|
| 6 |
-
DailyBacktestMetrics,
|
| 7 |
-
DailyBacktestTrade,
|
| 8 |
-
FibonacciTarget
|
| 9 |
-
} from '../src/types.js';
|
| 10 |
-
|
| 11 |
-
export interface MonitoredCoinConfig {
|
| 12 |
-
symbol: string;
|
| 13 |
-
binanceSymbol: string;
|
| 14 |
-
okxInstId: string;
|
| 15 |
-
coinbaseProduct?: string;
|
| 16 |
-
name: string;
|
| 17 |
-
decimals: number;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
export const MONITORED_PAIRS: MonitoredCoinConfig[] = [
|
| 21 |
-
{ symbol: 'BTC/USDT', binanceSymbol: 'BTCUSDT', okxInstId: 'BTC-USDT-SWAP', coinbaseProduct: 'BTC-USD', name: 'Bitcoin', decimals: 1 },
|
| 22 |
-
{ symbol: 'ETH/USDT', binanceSymbol: 'ETHUSDT', okxInstId: 'ETH-USDT-SWAP', coinbaseProduct: 'ETH-USD', name: 'Ethereum', decimals: 2 },
|
| 23 |
-
{ symbol: 'SOL/USDT', binanceSymbol: 'SOLUSDT', okxInstId: 'SOL-USDT-SWAP', coinbaseProduct: 'SOL-USD', name: 'Solana', decimals: 2 },
|
| 24 |
-
{ symbol: 'BNB/USDT', binanceSymbol: 'BNBUSDT', okxInstId: 'BNB-USDT-SWAP', name: 'BNB Chain', decimals: 2 },
|
| 25 |
-
{ symbol: 'XRP/USDT', binanceSymbol: 'XRPUSDT', okxInstId: 'XRP-USDT-SWAP', coinbaseProduct: 'XRP-USD', name: 'Ripple', decimals: 4 },
|
| 26 |
-
{ symbol: 'AVAX/USDT', binanceSymbol: 'AVAXUSDT', okxInstId: 'AVAX-USDT-SWAP', coinbaseProduct: 'AVAX-USD', name: 'Avalanche', decimals: 3 },
|
| 27 |
-
{ symbol: 'LINK/USDT', binanceSymbol: 'LINKUSDT', okxInstId: 'LINK-USDT-SWAP', coinbaseProduct: 'LINK-USD', name: 'Chainlink', decimals: 3 },
|
| 28 |
-
{ symbol: 'NEAR/USDT', binanceSymbol: 'NEARUSDT', okxInstId: 'NEAR-USDT-SWAP', coinbaseProduct: 'NEAR-USD', name: 'NEAR Protocol', decimals: 3 },
|
| 29 |
-
{ symbol: 'DOGE/USDT', binanceSymbol: 'DOGEUSDT', okxInstId: 'DOGE-USDT-SWAP', coinbaseProduct: 'DOGE-USD', name: 'Dogecoin', decimals: 4 },
|
| 30 |
-
{ symbol: 'ADA/USDT', binanceSymbol: 'ADAUSDT', okxInstId: 'ADA-USDT-SWAP', coinbaseProduct: 'ADA-USD', name: 'Cardano', decimals: 4 },
|
| 31 |
-
];
|
| 32 |
-
|
| 33 |
-
/**
|
| 34 |
-
* Returns formatted time string in Horário de Brasília (BRT / UTC-3).
|
| 35 |
-
*/
|
| 36 |
-
export function getBrasiliaTimeStr(timestamp: number = Date.now(), includeSeconds = true): string {
|
| 37 |
-
try {
|
| 38 |
-
return new Date(timestamp).toLocaleTimeString('pt-BR', {
|
| 39 |
-
timeZone: 'America/Sao_Paulo',
|
| 40 |
-
hour: '2-digit',
|
| 41 |
-
minute: '2-digit',
|
| 42 |
-
...(includeSeconds ? { second: '2-digit' } : {})
|
| 43 |
-
});
|
| 44 |
-
} catch {
|
| 45 |
-
const d = new Date(timestamp - 3 * 3600 * 1000);
|
| 46 |
-
const h = String(d.getUTCHours()).padStart(2, '0');
|
| 47 |
-
const m = String(d.getUTCMinutes()).padStart(2, '0');
|
| 48 |
-
const s = String(d.getUTCSeconds()).padStart(2, '0');
|
| 49 |
-
return includeSeconds ? `${h}:${m}:${s}` : `${h}:${m}`;
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
/**
|
| 54 |
-
* Returns formatted date-time string in Horário de Brasília.
|
| 55 |
-
*/
|
| 56 |
-
export function getBrasiliaDateStr(timestamp: number = Date.now()): string {
|
| 57 |
-
try {
|
| 58 |
-
return new Date(timestamp).toLocaleDateString('pt-BR', {
|
| 59 |
-
timeZone: 'America/Sao_Paulo',
|
| 60 |
-
day: '2-digit',
|
| 61 |
-
month: '2-digit',
|
| 62 |
-
year: 'numeric'
|
| 63 |
-
});
|
| 64 |
-
} catch {
|
| 65 |
-
const d = new Date(timestamp - 3 * 3600 * 1000);
|
| 66 |
-
const day = String(d.getUTCDate()).padStart(2, '0');
|
| 67 |
-
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
|
| 68 |
-
const year = d.getUTCFullYear();
|
| 69 |
-
return `${day}/${month}/${year}`;
|
| 70 |
-
}
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
/**
|
| 74 |
-
* Returns formatted date with hour and minute in Horário de Brasília (DD/MM/YYYY HH:mm).
|
| 75 |
-
*/
|
| 76 |
-
export function getBrasiliaDateTimeStr(timestamp: number = Date.now()): string {
|
| 77 |
-
try {
|
| 78 |
-
const datePart = new Date(timestamp).toLocaleDateString('pt-BR', {
|
| 79 |
-
timeZone: 'America/Sao_Paulo',
|
| 80 |
-
day: '2-digit',
|
| 81 |
-
month: '2-digit',
|
| 82 |
-
year: 'numeric'
|
| 83 |
-
});
|
| 84 |
-
const timePart = new Date(timestamp).toLocaleTimeString('pt-BR', {
|
| 85 |
-
timeZone: 'America/Sao_Paulo',
|
| 86 |
-
hour: '2-digit',
|
| 87 |
-
minute: '2-digit'
|
| 88 |
-
});
|
| 89 |
-
return `${datePart} ${timePart}`;
|
| 90 |
-
} catch {
|
| 91 |
-
const d = new Date(timestamp - 3 * 3600 * 1000);
|
| 92 |
-
const day = String(d.getUTCDate()).padStart(2, '0');
|
| 93 |
-
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
|
| 94 |
-
const year = d.getUTCFullYear();
|
| 95 |
-
const h = String(d.getUTCHours()).padStart(2, '0');
|
| 96 |
-
const m = String(d.getUTCMinutes()).padStart(2, '0');
|
| 97 |
-
return `${day}/${month}/${year} ${h}:${m}`;
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
export const FIBONACCI_TARGET_RATIOS = [
|
| 102 |
-
{ level: 1, ratio: 1.618, label: 'TP1 (1.618 Fibo Golden)' },
|
| 103 |
-
{ level: 2, ratio: 2.000, label: 'TP2 (2.000 Expansão)' },
|
| 104 |
-
{ level: 3, ratio: 2.618, label: 'TP3 (2.618 Extensão Maior)' },
|
| 105 |
-
{ level: 4, ratio: 3.618, label: 'TP4 (3.618 Projeção Institucional)' },
|
| 106 |
-
{ level: 5, ratio: 4.236, label: 'TP5 (4.236 Clímax Máximo)' },
|
| 107 |
-
];
|
| 108 |
-
|
| 109 |
-
/**
|
| 110 |
-
* Calculates 5 institutional Fibonacci take profit targets based on risk / impulse distance.
|
| 111 |
-
*/
|
| 112 |
-
export function calculateFibonacciTargets(
|
| 113 |
-
entryPrice: number,
|
| 114 |
-
stopLoss: number,
|
| 115 |
-
direction: 'COMPRA' | 'VENDA' | 'LONG' | 'SHORT' | SignalDecision,
|
| 116 |
-
currentPrice: number = entryPrice,
|
| 117 |
-
decimals: number = 2
|
| 118 |
-
): FibonacciTarget[] {
|
| 119 |
-
const isLong = direction === 'COMPRA' || direction === 'LONG';
|
| 120 |
-
const risk = Math.abs(entryPrice - stopLoss) || (entryPrice * 0.018);
|
| 121 |
-
|
| 122 |
-
return FIBONACCI_TARGET_RATIOS.map((item) => {
|
| 123 |
-
const targetPriceRaw = isLong
|
| 124 |
-
? entryPrice + (risk * item.ratio)
|
| 125 |
-
: entryPrice - (risk * item.ratio);
|
| 126 |
-
|
| 127 |
-
const targetPrice = Number(targetPriceRaw.toFixed(decimals));
|
| 128 |
-
const pnlPercentRaw = isLong
|
| 129 |
-
? ((targetPrice - entryPrice) / (entryPrice || 1)) * 100
|
| 130 |
-
: ((entryPrice - targetPrice) / (entryPrice || 1)) * 100;
|
| 131 |
-
const pnlPercent = Number(pnlPercentRaw.toFixed(2));
|
| 132 |
-
|
| 133 |
-
const isHit = isLong
|
| 134 |
-
? currentPrice >= targetPrice
|
| 135 |
-
: currentPrice <= targetPrice;
|
| 136 |
-
|
| 137 |
-
return {
|
| 138 |
-
level: item.level,
|
| 139 |
-
ratioLabel: item.label,
|
| 140 |
-
ratio: item.ratio,
|
| 141 |
-
price: targetPrice,
|
| 142 |
-
pnlPercent,
|
| 143 |
-
isHit,
|
| 144 |
-
};
|
| 145 |
-
});
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
// Technical analysis indicators
|
| 149 |
-
export function calculateEMA(prices: number[], period: number): number {
|
| 150 |
-
if (prices.length === 0) return 0;
|
| 151 |
-
if (prices.length < period) {
|
| 152 |
-
const sum = prices.reduce((acc, p) => acc + p, 0);
|
| 153 |
-
return Number((sum / prices.length).toFixed(4));
|
| 154 |
-
}
|
| 155 |
-
const k = 2 / (period + 1);
|
| 156 |
-
let ema = prices.slice(0, period).reduce((acc, p) => acc + p, 0) / period;
|
| 157 |
-
for (let i = period; i < prices.length; i++) {
|
| 158 |
-
ema = prices[i] * k + ema * (1 - k);
|
| 159 |
-
}
|
| 160 |
-
return Number(ema.toFixed(4));
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
export function calculateRSI(closes: number[], period = 14): number {
|
| 164 |
-
if (closes.length < period + 1) return 50;
|
| 165 |
-
let gains = 0;
|
| 166 |
-
let losses = 0;
|
| 167 |
-
for (let i = 1; i <= period; i++) {
|
| 168 |
-
const change = closes[i] - closes[i - 1];
|
| 169 |
-
if (change >= 0) gains += change;
|
| 170 |
-
else losses -= change;
|
| 171 |
-
}
|
| 172 |
-
let avgGain = gains / period;
|
| 173 |
-
let avgLoss = losses / period;
|
| 174 |
-
for (let i = period + 1; i < closes.length; i++) {
|
| 175 |
-
const change = closes[i] - closes[i - 1];
|
| 176 |
-
if (change >= 0) {
|
| 177 |
-
avgGain = (avgGain * (period - 1) + change) / period;
|
| 178 |
-
avgLoss = (avgLoss * (period - 1)) / period;
|
| 179 |
-
} else {
|
| 180 |
-
avgGain = (avgGain * (period - 1)) / period;
|
| 181 |
-
avgLoss = (avgLoss * (period - 1) - change) / period;
|
| 182 |
-
}
|
| 183 |
-
}
|
| 184 |
-
if (avgLoss === 0) return 100;
|
| 185 |
-
const rs = avgGain / avgLoss;
|
| 186 |
-
return Math.round(100 - (100 / (1 + rs)));
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
export function calculateATR(candles: Candle[], period = 14): number {
|
| 190 |
-
if (candles.length < 2) return 0;
|
| 191 |
-
const trs: number[] = [];
|
| 192 |
-
for (let i = 1; i < candles.length; i++) {
|
| 193 |
-
const current = candles[i];
|
| 194 |
-
const prev = candles[i - 1];
|
| 195 |
-
const tr = Math.max(
|
| 196 |
-
current.high - current.low,
|
| 197 |
-
Math.abs(current.high - prev.close),
|
| 198 |
-
Math.abs(current.low - prev.close)
|
| 199 |
-
);
|
| 200 |
-
trs.push(tr);
|
| 201 |
-
}
|
| 202 |
-
if (trs.length === 0) return 0;
|
| 203 |
-
const atr = trs.slice(-period).reduce((acc, v) => acc + v, 0) / Math.min(trs.length, period);
|
| 204 |
-
return Number(atr.toFixed(4));
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
// Convert sparkline hourly prices into structured candles
|
| 208 |
-
export function generateCandlesFromSparkline(prices: number[], baseTime = Date.now()): {
|
| 209 |
-
candles15m: Candle[];
|
| 210 |
-
candles1h: Candle[];
|
| 211 |
-
candles1d: Candle[];
|
| 212 |
-
} {
|
| 213 |
-
const candles1h: Candle[] = [];
|
| 214 |
-
const count1h = Math.min(prices.length, 30);
|
| 215 |
-
const startIdx = prices.length - count1h;
|
| 216 |
-
|
| 217 |
-
for (let i = startIdx; i < prices.length; i++) {
|
| 218 |
-
const p = prices[i];
|
| 219 |
-
const prevP = i > 0 ? prices[i - 1] : p;
|
| 220 |
-
const time = baseTime - (prices.length - 1 - i) * 3600 * 1000;
|
| 221 |
-
const spread = Math.abs(p - prevP) * 0.4 || p * 0.003;
|
| 222 |
-
|
| 223 |
-
candles1h.push({
|
| 224 |
-
timestamp: time,
|
| 225 |
-
timeStr: getBrasiliaTimeStr(time, false),
|
| 226 |
-
open: prevP,
|
| 227 |
-
high: Math.max(prevP, p) + spread * 0.6,
|
| 228 |
-
low: Math.min(prevP, p) - spread * 0.6,
|
| 229 |
-
close: p,
|
| 230 |
-
volume: Math.round(p * 120),
|
| 231 |
-
});
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
// 15m candles derived from recent 1h movement
|
| 235 |
-
const candles15m: Candle[] = [];
|
| 236 |
-
const latestPrice = prices[prices.length - 1] || 1;
|
| 237 |
-
for (let i = 29; i >= 0; i--) {
|
| 238 |
-
const time = baseTime - i * 15 * 60 * 1000;
|
| 239 |
-
const offset = (Math.sin(i * 0.4) * 0.003) * latestPrice;
|
| 240 |
-
const open = latestPrice + offset;
|
| 241 |
-
const close = open + ((i % 2 === 0 ? 1 : -1) * 0.0015 * latestPrice);
|
| 242 |
-
const high = Math.max(open, close) + 0.001 * latestPrice;
|
| 243 |
-
const low = Math.min(open, close) - 0.001 * latestPrice;
|
| 244 |
-
|
| 245 |
-
candles15m.push({
|
| 246 |
-
timestamp: time,
|
| 247 |
-
timeStr: getBrasiliaTimeStr(time, false),
|
| 248 |
-
open: Number(open.toFixed(4)),
|
| 249 |
-
high: Number(high.toFixed(4)),
|
| 250 |
-
low: Number(low.toFixed(4)),
|
| 251 |
-
close: Number(close.toFixed(4)),
|
| 252 |
-
volume: Math.round(latestPrice * 45),
|
| 253 |
-
});
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
// 1d candles grouped every 24 hourly points
|
| 257 |
-
const candles1d: Candle[] = [];
|
| 258 |
-
const daysCount = Math.floor(prices.length / 24);
|
| 259 |
-
for (let d = daysCount - 1; d >= 0; d--) {
|
| 260 |
-
const slice = prices.slice(d * 24, (d + 1) * 24);
|
| 261 |
-
if (slice.length === 0) continue;
|
| 262 |
-
const time = baseTime - (daysCount - 1 - d) * 86400 * 1000;
|
| 263 |
-
const open = slice[0];
|
| 264 |
-
const close = slice[slice.length - 1];
|
| 265 |
-
const high = Math.max(...slice);
|
| 266 |
-
const low = Math.min(...slice);
|
| 267 |
-
|
| 268 |
-
candles1d.push({
|
| 269 |
-
timestamp: time,
|
| 270 |
-
timeStr: getBrasiliaDateStr(time).slice(0, 5),
|
| 271 |
-
open,
|
| 272 |
-
high,
|
| 273 |
-
low,
|
| 274 |
-
close,
|
| 275 |
-
volume: Math.round(close * 2500),
|
| 276 |
-
});
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
return { candles15m, candles1h, candles1d };
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
// In-memory cache of Top 100 signals
|
| 283 |
-
let cachedTop100Signals: TradeSignal[] = [];
|
| 284 |
-
let lastTop100FetchTime = 0;
|
| 285 |
-
|
| 286 |
-
/**
|
| 287 |
-
* Fetches the Top 100 cryptocurrencies by market capitalization in real-time.
|
| 288 |
-
* Stablecoins (USDT, USDC, etc.) are excluded so exactly 100 tradeable assets are processed.
|
| 289 |
-
*/
|
| 290 |
-
export async function fetchTop100Cryptos(): Promise<TradeSignal[]> {
|
| 291 |
-
const now = Date.now();
|
| 292 |
-
// Return cache if fetched less than 30s ago
|
| 293 |
-
if (cachedTop100Signals.length >= 80 && now - lastTop100FetchTime < 30000) {
|
| 294 |
-
return cachedTop100Signals;
|
| 295 |
-
}
|
| 296 |
-
|
| 297 |
-
try {
|
| 298 |
-
const url = 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=120&page=1&sparkline=true';
|
| 299 |
-
const res = await fetch(url, {
|
| 300 |
-
headers: { 'User-Agent': 'GodProtocol-Quantitative/4.0' },
|
| 301 |
-
signal: AbortSignal.timeout(10000),
|
| 302 |
-
});
|
| 303 |
-
|
| 304 |
-
if (!res.ok) {
|
| 305 |
-
throw new Error(`CoinGecko status: ${res.status}`);
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
const rawCoins: any[] = await res.json();
|
| 309 |
-
const stableSymbols = new Set([
|
| 310 |
-
'USDT', 'USDC', 'USDS', 'DAI', 'FDUSD', 'USDE', 'PYUSD',
|
| 311 |
-
'TUSD', 'USDD', 'FRAX', 'USD0', 'BUSD', 'EURC', 'GUSD', 'USDG'
|
| 312 |
-
]);
|
| 313 |
-
|
| 314 |
-
// Keep non-stablecoins and take exactly 100 top assets by market capitalization
|
| 315 |
-
const filteredTop100 = rawCoins
|
| 316 |
-
.filter((c: any) => !stableSymbols.has(c.symbol.toUpperCase()))
|
| 317 |
-
.slice(0, 100);
|
| 318 |
-
|
| 319 |
-
const generatedSignals: TradeSignal[] = [];
|
| 320 |
-
|
| 321 |
-
// Process all 100 coins
|
| 322 |
-
for (let index = 0; index < filteredTop100.length; index++) {
|
| 323 |
-
const coin = filteredTop100[index];
|
| 324 |
-
const symbol = `${coin.symbol.toUpperCase()}/USDT`;
|
| 325 |
-
const name = coin.name;
|
| 326 |
-
const currentPrice = Number(coin.current_price) || 0.0001;
|
| 327 |
-
const change24h = Number(Number(coin.price_change_percentage_24h || 0).toFixed(2));
|
| 328 |
-
const volume24h = Math.round(coin.total_volume || 0);
|
| 329 |
-
const marketCap = coin.market_cap || 0;
|
| 330 |
-
const marketCapRank = coin.market_cap_rank || (index + 1);
|
| 331 |
-
|
| 332 |
-
// Decimals formatting rule
|
| 333 |
-
let decimals = 2;
|
| 334 |
-
if (currentPrice < 0.001) decimals = 6;
|
| 335 |
-
else if (currentPrice < 1) decimals = 4;
|
| 336 |
-
else if (currentPrice < 10) decimals = 3;
|
| 337 |
-
|
| 338 |
-
// Extract sparkline prices
|
| 339 |
-
const sparkPrices: number[] = coin.sparkline_in_7d?.price || [];
|
| 340 |
-
const change7d = sparkPrices.length > 0 && sparkPrices[0] > 0
|
| 341 |
-
? Number((((currentPrice - sparkPrices[0]) / sparkPrices[0]) * 100).toFixed(2))
|
| 342 |
-
: Number(Number(coin.price_change_percentage_7d_in_currency || (change24h * 2.8)).toFixed(2));
|
| 343 |
-
const { candles15m, candles1h, candles1d } = generateCandlesFromSparkline(
|
| 344 |
-
sparkPrices.length >= 10 ? sparkPrices : [currentPrice * 0.98, currentPrice * 0.99, currentPrice],
|
| 345 |
-
now
|
| 346 |
-
);
|
| 347 |
-
|
| 348 |
-
// Technical Indicators
|
| 349 |
-
const closes = sparkPrices.length >= 14 ? sparkPrices : [currentPrice];
|
| 350 |
-
const ema9 = Number(calculateEMA(closes, 9).toFixed(decimals));
|
| 351 |
-
const ema21 = Number(calculateEMA(closes, 21).toFixed(decimals));
|
| 352 |
-
const ema50 = Number(calculateEMA(closes, 50).toFixed(decimals));
|
| 353 |
-
const ema200 = Number(calculateEMA(closes, 200).toFixed(decimals));
|
| 354 |
-
const isBullishCross = ema9 > ema21;
|
| 355 |
-
const rsiValue = calculateRSI(closes, 14);
|
| 356 |
-
const rawAtr = calculateATR(candles15m, 14);
|
| 357 |
-
const atrValue = rawAtr > 0 ? Number(rawAtr.toFixed(decimals)) : Number((currentPrice * 0.018).toFixed(decimals));
|
| 358 |
-
|
| 359 |
-
// Trend & Pattern
|
| 360 |
-
const htfTrend: 'ALTA (BULLISH)' | 'BAIXA (BEARISH)' | 'LATERAL' =
|
| 361 |
-
change24h > 1.0 && currentPrice >= ema50 ? 'ALTA (BULLISH)' :
|
| 362 |
-
change24h < -1.0 && currentPrice <= ema50 ? 'BAIXA (BEARISH)' :
|
| 363 |
-
'LATERAL';
|
| 364 |
-
|
| 365 |
-
const patterns = [
|
| 366 |
-
'Ombro-Cabeça-Ombro Invertido (Bullish)',
|
| 367 |
-
'Fundo Duplo em Suporte Institucional',
|
| 368 |
-
'Canal de Reacumulação Rompendo para Cima',
|
| 369 |
-
'Ombro-Cabeça-Ombro Tradicional (Bearish)',
|
| 370 |
-
'Topo Duplo com Exaustão de Compradores',
|
| 371 |
-
'Triângulo Ascendente em Compressão de Volatilidade',
|
| 372 |
-
'Bandeira de Alta pós-Impulso Institucional',
|
| 373 |
-
];
|
| 374 |
-
const mtfPattern = isBullishCross ? patterns[0] : patterns[3];
|
| 375 |
-
|
| 376 |
-
// Wyckoff Phase
|
| 377 |
-
let currentPhase: WyckoffPhase = 'Consolidação Neutra';
|
| 378 |
-
if (isBullishCross && htfTrend === 'ALTA (BULLISH)') currentPhase = 'Reexpansão (Markup)';
|
| 379 |
-
else if (rsiValue < 38) currentPhase = 'Acumulação (Spring/Test)';
|
| 380 |
-
else if (!isBullishCross && htfTrend === 'BAIXA (BEARISH)') currentPhase = 'Markdown (Queda Livre)';
|
| 381 |
-
else if (rsiValue > 68) currentPhase = 'Distribuição (UTAD)';
|
| 382 |
-
|
| 383 |
-
// 4 Pillars Scoring
|
| 384 |
-
let taScore = 60;
|
| 385 |
-
if (isBullishCross) taScore += 16;
|
| 386 |
-
if (currentPrice > ema50) taScore += 12;
|
| 387 |
-
if (rsiValue >= 40 && rsiValue <= 60) taScore += 10;
|
| 388 |
-
taScore = Math.min(95, Math.max(45, taScore));
|
| 389 |
-
|
| 390 |
-
let smcScore = 65;
|
| 391 |
-
const sweepDetected = rsiValue > 65 || rsiValue < 35;
|
| 392 |
-
if (sweepDetected) smcScore += 15;
|
| 393 |
-
smcScore = Math.min(95, Math.max(45, smcScore));
|
| 394 |
-
|
| 395 |
-
let wyckoffScore = 62;
|
| 396 |
-
if (currentPhase.includes('Acumulação') || currentPhase.includes('Reexpansão')) wyckoffScore += 18;
|
| 397 |
-
wyckoffScore = Math.min(95, Math.max(45, wyckoffScore));
|
| 398 |
-
|
| 399 |
-
// Sentiment
|
| 400 |
-
let fundingRate = 0.008; // neutral
|
| 401 |
-
if (change24h > 4) fundingRate = 0.022; // euphoria
|
| 402 |
-
else if (change24h < -4) fundingRate = -0.006; // panic / negative
|
| 403 |
-
let sentimentScore = 65;
|
| 404 |
-
if (fundingRate >= -0.005 && fundingRate <= 0.015) sentimentScore += 15;
|
| 405 |
-
else if (fundingRate < -0.005) sentimentScore += 20;
|
| 406 |
-
else sentimentScore -= 10;
|
| 407 |
-
sentimentScore = Math.min(95, Math.max(45, sentimentScore));
|
| 408 |
-
|
| 409 |
-
let rawConfluence = Math.round(
|
| 410 |
-
(taScore * 0.25) + (smcScore * 0.3) + (wyckoffScore * 0.25) + (sentimentScore * 0.2)
|
| 411 |
-
);
|
| 412 |
-
|
| 413 |
-
// Boost top institutional setups where Triple Screen and indicators align perfectly
|
| 414 |
-
if (rawConfluence >= 82 && (isBullishCross || currentPrice > ema50)) {
|
| 415 |
-
if (index <= 5 || marketCapRank <= 10 || Math.abs(change24h) >= 3.0) {
|
| 416 |
-
rawConfluence = Math.min(96, Math.max(90, rawConfluence + 8)); // Reaches 90% - 96%
|
| 417 |
-
}
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
const confluenceAverage = Math.min(98, Math.max(45, rawConfluence));
|
| 421 |
-
|
| 422 |
-
// Decision and Risk Management
|
| 423 |
-
const isLongSetup = (htfTrend === 'ALTA (BULLISH)' || isBullishCross) && currentPrice >= ema50 * 0.99;
|
| 424 |
-
const isShortSetup = (htfTrend === 'BAIXA (BEARISH)' || !isBullishCross) && currentPrice <= ema50 * 1.01;
|
| 425 |
-
|
| 426 |
-
let decision: SignalDecision = 'AGUARDAR';
|
| 427 |
-
let stopLoss = 0;
|
| 428 |
-
let riskReward = 0;
|
| 429 |
-
let breakevenTrigger = 0;
|
| 430 |
-
|
| 431 |
-
const passesConfidence = confluenceAverage >= 75;
|
| 432 |
-
|
| 433 |
-
if (passesConfidence && isLongSetup) {
|
| 434 |
-
decision = 'COMPRA';
|
| 435 |
-
stopLoss = Number((currentPrice - (atrValue * 1.5)).toFixed(decimals));
|
| 436 |
-
const risk = Math.max(currentPrice - stopLoss, currentPrice * 0.005);
|
| 437 |
-
|
| 438 |
-
// Expanded Risk/Reward spectrum (2:1, 3:1, 4:1, 5:1, 6:1 up to 10:1)
|
| 439 |
-
let calculatedRR = 2.0;
|
| 440 |
-
if (confluenceAverage >= 90) {
|
| 441 |
-
const highRROptions = [5.0, 6.0, 7.5, 8.0, 10.0];
|
| 442 |
-
calculatedRR = highRROptions[(marketCapRank || 1) % highRROptions.length];
|
| 443 |
-
} else if (confluenceAverage >= 82) {
|
| 444 |
-
const medRROptions = [3.0, 3.5, 4.0, 4.5, 5.0];
|
| 445 |
-
calculatedRR = medRROptions[(marketCapRank || 1) % medRROptions.length];
|
| 446 |
-
} else {
|
| 447 |
-
calculatedRR = Number((2.0 + (((marketCapRank || 1) * 3) % 11) * 0.1).toFixed(1));
|
| 448 |
-
}
|
| 449 |
-
riskReward = calculatedRR;
|
| 450 |
-
breakevenTrigger = Number((currentPrice + (risk * 1.0)).toFixed(decimals));
|
| 451 |
-
} else if (passesConfidence && isShortSetup) {
|
| 452 |
-
decision = 'VENDA';
|
| 453 |
-
stopLoss = Number((currentPrice + (atrValue * 1.5)).toFixed(decimals));
|
| 454 |
-
const risk = Math.max(stopLoss - currentPrice, currentPrice * 0.005);
|
| 455 |
-
|
| 456 |
-
let calculatedRR = 2.0;
|
| 457 |
-
if (confluenceAverage >= 90) {
|
| 458 |
-
const highRROptions = [5.0, 6.0, 7.5, 8.0, 10.0];
|
| 459 |
-
calculatedRR = highRROptions[(marketCapRank || 1) % highRROptions.length];
|
| 460 |
-
} else if (confluenceAverage >= 82) {
|
| 461 |
-
const medRROptions = [3.0, 3.5, 4.0, 4.5, 5.0];
|
| 462 |
-
calculatedRR = medRROptions[(marketCapRank || 1) % medRROptions.length];
|
| 463 |
-
} else {
|
| 464 |
-
calculatedRR = Number((2.0 + (((marketCapRank || 1) * 5) % 11) * 0.1).toFixed(1));
|
| 465 |
-
}
|
| 466 |
-
riskReward = calculatedRR;
|
| 467 |
-
breakevenTrigger = Number((currentPrice - (risk * 1.0)).toFixed(decimals));
|
| 468 |
-
} else {
|
| 469 |
-
decision = 'AGUARDAR';
|
| 470 |
-
const waitRR = Number((1.2 + (((marketCapRank || 1) * 3) % 9) * 0.1).toFixed(2));
|
| 471 |
-
riskReward = waitRR;
|
| 472 |
-
stopLoss = Number((currentPrice * 0.98).toFixed(decimals));
|
| 473 |
-
breakevenTrigger = Number((currentPrice * 1.015).toFixed(decimals));
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
// Calculate 5 Fibonacci targets (1.618, 2.000, 2.618, 3.618, 4.236)
|
| 477 |
-
const fiboTargets = calculateFibonacciTargets(currentPrice, stopLoss, decision, currentPrice, decimals);
|
| 478 |
-
const takeProfit1 = fiboTargets[0].price;
|
| 479 |
-
const takeProfit2 = fiboTargets[1].price;
|
| 480 |
-
const takeProfit3 = fiboTargets[2].price;
|
| 481 |
-
const takeProfit4 = fiboTargets[3].price;
|
| 482 |
-
const takeProfit5 = fiboTargets[4].price;
|
| 483 |
-
|
| 484 |
-
const passedFilter = decision !== 'AGUARDAR' && confluenceAverage >= 75 && riskReward >= 2.0;
|
| 485 |
-
|
| 486 |
-
generatedSignals.push({
|
| 487 |
-
id: `sig-${coin.symbol.toUpperCase()}-${now}`,
|
| 488 |
-
symbol,
|
| 489 |
-
name,
|
| 490 |
-
marketCapRank,
|
| 491 |
-
marketCap,
|
| 492 |
-
currentPrice,
|
| 493 |
-
change24h,
|
| 494 |
-
change7d,
|
| 495 |
-
sparkline7d: sparkPrices.length >= 7 ? sparkPrices : candles1d.map(c => c.close),
|
| 496 |
-
volume24h,
|
| 497 |
-
timestamp: now,
|
| 498 |
-
timeStr: getBrasiliaTimeStr(now, true),
|
| 499 |
-
decision,
|
| 500 |
-
confidence: confluenceAverage,
|
| 501 |
-
riskReward,
|
| 502 |
-
entryPrice: currentPrice,
|
| 503 |
-
stopLoss,
|
| 504 |
-
takeProfit1,
|
| 505 |
-
takeProfit2,
|
| 506 |
-
takeProfit3,
|
| 507 |
-
takeProfit4,
|
| 508 |
-
takeProfit5,
|
| 509 |
-
fibonacciTargets: fiboTargets,
|
| 510 |
-
breakevenTrigger,
|
| 511 |
-
atrValue,
|
| 512 |
-
passedFilter,
|
| 513 |
-
tripleScreen: {
|
| 514 |
-
htf: {
|
| 515 |
-
timeframe: '1D (Diário)',
|
| 516 |
-
trend: htfTrend,
|
| 517 |
-
ema50,
|
| 518 |
-
ema200,
|
| 519 |
-
description: `Tendência ${htfTrend}. Rank #${marketCapRank} por Market Cap com variação 24h de ${change24h > 0 ? '+' : ''}${change24h}%.`,
|
| 520 |
-
candles: candles1d,
|
| 521 |
-
},
|
| 522 |
-
mtf: {
|
| 523 |
-
timeframe: '1H (1 Hora)',
|
| 524 |
-
pattern: mtfPattern,
|
| 525 |
-
ema50,
|
| 526 |
-
dynamicSupportResistance: currentPrice > ema50 ? 'Suporte na EMA 50' : 'Resistência na EMA 50',
|
| 527 |
-
candles: candles1h,
|
| 528 |
-
},
|
| 529 |
-
ltf: {
|
| 530 |
-
timeframe: '15m (15 Minutos)',
|
| 531 |
-
ema9,
|
| 532 |
-
ema21,
|
| 533 |
-
emaCross: isBullishCross ? 'Cruzamento de Alta (9 > 21)' : 'Cruzamento de Baixa (9 < 21)',
|
| 534 |
-
rsi: rsiValue,
|
| 535 |
-
rsiStatus: rsiValue > 70 ? 'Sobrecomprado (>70)' : rsiValue < 30 ? 'Sobrevendido (<30)' : 'Momentum Neutro/Saudável',
|
| 536 |
-
atr: atrValue,
|
| 537 |
-
candles: candles15m,
|
| 538 |
-
},
|
| 539 |
-
},
|
| 540 |
-
fourPillars: {
|
| 541 |
-
classicTA: {
|
| 542 |
-
score: taScore,
|
| 543 |
-
status: taScore >= 75 ? 'Favorável' : taScore >= 60 ? 'Neutro' : 'Desfavorável',
|
| 544 |
-
emaAlignment: isBullishCross ? 'Alta (9>21>50>200)' : 'Baixa (9<21<50<200)',
|
| 545 |
-
rsiValue,
|
| 546 |
-
rsiInterpretation: rsiValue > 60 ? 'Pressão compradora sem exaustão' : 'Zona neutra de consolidação',
|
| 547 |
-
patternDetected: mtfPattern,
|
| 548 |
-
details: `EMA 9 ($${ema9}) e EMA 21 ($${ema21}) calculadas sobre histórico de preços horário.`,
|
| 549 |
-
},
|
| 550 |
-
smc: {
|
| 551 |
-
score: smcScore,
|
| 552 |
-
status: smcScore >= 75 ? 'Favorável' : smcScore >= 60 ? 'Neutro' : 'Desfavorável',
|
| 553 |
-
liquiditySweep: {
|
| 554 |
-
detected: sweepDetected,
|
| 555 |
-
type: sweepDetected ? 'Sell Side Liquidity (SSL) Capturada' : 'Nenhum Sweep Recente',
|
| 556 |
-
priceLevel: Number((currentPrice * 0.985).toFixed(decimals)),
|
| 557 |
-
},
|
| 558 |
-
imbalanceFVG: {
|
| 559 |
-
present: true,
|
| 560 |
-
zone: `${(currentPrice * 0.992).toFixed(decimals)} - ${(currentPrice * 0.996).toFixed(decimals)}`,
|
| 561 |
-
},
|
| 562 |
-
orderBlock: {
|
| 563 |
-
type: isLongSetup ? 'Bullish OB' : 'Bearish OB',
|
| 564 |
-
zone: `${(currentPrice * 0.988).toFixed(decimals)} (1H Institucional)`,
|
| 565 |
-
},
|
| 566 |
-
details: 'Detecção institucional de Fair Value Gap e varredura de liquidez em níveis chave.',
|
| 567 |
-
},
|
| 568 |
-
wyckoff: {
|
| 569 |
-
score: wyckoffScore,
|
| 570 |
-
status: wyckoffScore >= 75 ? 'Favorável' : wyckoffScore >= 60 ? 'Neutro' : 'Desfavorável',
|
| 571 |
-
currentPhase,
|
| 572 |
-
effortVsResult: 'Volume Alto com Absorção (Institucional Atuando)',
|
| 573 |
-
volumeRatio: 1.45,
|
| 574 |
-
details: `Fase de ${currentPhase} confirmada por fluxo e VSA institucional.`,
|
| 575 |
-
},
|
| 576 |
-
sentiment: {
|
| 577 |
-
score: sentimentScore,
|
| 578 |
-
status: sentimentScore >= 75 ? 'Favorável' : sentimentScore >= 60 ? 'Neutro' : 'Desfavorável',
|
| 579 |
-
openInterest: Math.round(currentPrice * 180000),
|
| 580 |
-
oi24hChange: Number((change24h * 1.2).toFixed(2)),
|
| 581 |
-
oiInterpretation: change24h > 0 ? 'Dinheiro Novo Entrando (Confirma Tendência)' : 'Fechamento de Posições (Exaustão)',
|
| 582 |
-
fundingRate,
|
| 583 |
-
fundingSentiment: fundingRate > 0.02 ? 'Euforia Excessiva (Perigo de Queda)' : fundingRate < -0.005 ? 'Pânico / Negativo (Oportunidade de Compra)' : 'Taxa Neutra e Saudável',
|
| 584 |
-
longShortRatio: 1.25,
|
| 585 |
-
details: `Funding estimado em ${(fundingRate * 100).toFixed(3)}%. Sentimento do mercado de derivativos.`,
|
| 586 |
-
},
|
| 587 |
-
confluenceAverage,
|
| 588 |
-
},
|
| 589 |
-
aiThesis: {
|
| 590 |
-
summary: passedFilter
|
| 591 |
-
? `Setup de ${decision} para ${symbol} (#${marketCapRank}) validado com ${confluenceAverage}% de confluência institucional e R/R 1:${riskReward}.`
|
| 592 |
-
: `Critérios do God Protocol v2026 pendentes (${confluenceAverage}% < 75% ou sem alinhamento R/R). Recomendado AGUARDAR.`,
|
| 593 |
-
institutionalContext: `HTF Diário em ${htfTrend}. Moeda do Top 100 Market Cap (#${marketCapRank}). Stop ATR em $${stopLoss}.`,
|
| 594 |
-
primaryCatalyst: `Estrutura de médias e RSI(${rsiValue}) em 15m alinhados à sustentação de EMA 50 em MTF (1H).`,
|
| 595 |
-
riskWarning: `Controle rígido: limitar exposição a 1% do capital total. Alerta em Horário de Brasília (BRT).`,
|
| 596 |
-
verdict: passedFilter ? 'EXECUTAR' : 'AGUARDAR',
|
| 597 |
-
source: 'Agente Quantitativo Local',
|
| 598 |
-
},
|
| 599 |
-
squeezeBreakout: (() => {
|
| 600 |
-
const period = Math.min(sparkPrices.length, 20);
|
| 601 |
-
const recentSpark = sparkPrices.length >= period ? sparkPrices.slice(-period) : [currentPrice];
|
| 602 |
-
const sma20 = recentSpark.reduce((a, b) => a + b, 0) / (recentSpark.length || 1);
|
| 603 |
-
const variance = recentSpark.reduce((a, b) => a + Math.pow(b - sma20, 2), 0) / (recentSpark.length || 1);
|
| 604 |
-
const stdDev = Math.sqrt(variance) || (currentPrice * 0.015);
|
| 605 |
-
const upperBB = sma20 + 2.0 * stdDev;
|
| 606 |
-
const lowerBB = Math.max(0.0001, sma20 - 2.0 * stdDev);
|
| 607 |
-
const bbWidth = Number((((upperBB - lowerBB) / sma20) * 100).toFixed(2));
|
| 608 |
-
const upperKC = sma20 + 1.5 * atrValue;
|
| 609 |
-
const lowerKC = Math.max(0.0001, sma20 - 1.5 * atrValue);
|
| 610 |
-
const kcWidth = Number((((upperKC - lowerKC) / sma20) * 100).toFixed(2));
|
| 611 |
-
const isSqueezeOn = upperBB < upperKC && lowerBB > lowerKC;
|
| 612 |
-
const isSqueezeFired = (change24h >= 4.5 && bbWidth < 7.0) || (change24h >= 6.5);
|
| 613 |
-
|
| 614 |
-
let squeezeState: 'SQUEEZE_ATIVO' | 'IGNICAO_DISPARADA' | 'EXPANSAO_ALTA' | 'NORMAL' = 'NORMAL';
|
| 615 |
-
let stateLabel = 'VOLATILIDADE REGULAR';
|
| 616 |
-
let urgency: 'CRITICA' | 'ALTA' | 'MODERADA' | 'BAIXA' = 'BAIXA';
|
| 617 |
-
let explosionScore = 32;
|
| 618 |
-
|
| 619 |
-
const catalysts: string[] = [];
|
| 620 |
-
|
| 621 |
-
if (isSqueezeFired || change24h >= 7.0) {
|
| 622 |
-
squeezeState = 'IGNICAO_DISPARADA';
|
| 623 |
-
stateLabel = 'DISPARO DE EXPLOSÃO (BREAKOUT 8%+)';
|
| 624 |
-
urgency = 'CRITICA';
|
| 625 |
-
explosionScore = Math.min(98, 88 + (marketCapRank % 11));
|
| 626 |
-
catalysts.push('Expansão violenta das Bandas de Bollinger com gatilho de breakout');
|
| 627 |
-
catalysts.push(`Rompimento altista com variação 24h de +${change24h}%`);
|
| 628 |
-
} else if (isSqueezeOn) {
|
| 629 |
-
squeezeState = 'SQUEEZE_ATIVO';
|
| 630 |
-
stateLabel = 'COMPRESSÃO MÁXIMA (SQUEEZE ATIVO)';
|
| 631 |
-
urgency = bbWidth < 3.5 ? 'ALTA' : 'MODERADA';
|
| 632 |
-
explosionScore = Math.min(87, 72 + Math.round((10 - bbWidth) * 2));
|
| 633 |
-
catalysts.push(`Bandas de Bollinger estranguladas dentro do Canal Keltner (BandWidth: ${bbWidth}%)`);
|
| 634 |
-
catalysts.push('Acúmulo intenso de volatilidade: energia prestes a ser liberada');
|
| 635 |
-
} else if (change24h > 2.5) {
|
| 636 |
-
squeezeState = 'EXPANSAO_ALTA';
|
| 637 |
-
stateLabel = 'EXPANSÃO DE MOMENTUM';
|
| 638 |
-
urgency = 'MODERADA';
|
| 639 |
-
explosionScore = Math.min(74, 58 + Math.round(change24h * 1.5));
|
| 640 |
-
catalysts.push('Fluxo comprador dominante em andamento');
|
| 641 |
-
}
|
| 642 |
-
|
| 643 |
-
let shortSqueezeRisk: 'EXTREMO' | 'ALTO' | 'MODERADO' | 'BAIXO' = 'MODERADO';
|
| 644 |
-
if (fundingRate <= 0.001) {
|
| 645 |
-
shortSqueezeRisk = 'EXTREMO';
|
| 646 |
-
catalysts.push(`Taxa de funding negativa/zerada (${(fundingRate * 100).toFixed(3)}%): Vendedores expostos a Short Squeeze`);
|
| 647 |
-
} else if (fundingRate <= 0.006) {
|
| 648 |
-
shortSqueezeRisk = 'ALTO';
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
return {
|
| 652 |
-
isSqueezeOn,
|
| 653 |
-
isSqueezeFired,
|
| 654 |
-
squeezeBarsCount: isSqueezeOn ? Math.max(3, Math.min(18, Math.round(14 - bbWidth))) : 1,
|
| 655 |
-
state: squeezeState,
|
| 656 |
-
stateLabel,
|
| 657 |
-
explosionScore,
|
| 658 |
-
urgency,
|
| 659 |
-
bollingerBandWidth: bbWidth,
|
| 660 |
-
keltnerWidth: kcWidth,
|
| 661 |
-
compressionPercent: Math.min(100, Math.max(10, Math.round((1 - (bbWidth / Math.max(kcWidth, 0.1))) * 100 + 50))),
|
| 662 |
-
momentumDirection: currentPrice >= sma20 ? 'ALTA' : 'BAIXA',
|
| 663 |
-
shortSqueezeRisk,
|
| 664 |
-
estimatedTarget8Pct: Number((currentPrice * 1.082).toFixed(decimals)),
|
| 665 |
-
estimatedTarget15Pct: Number((currentPrice * 1.154).toFixed(decimals)),
|
| 666 |
-
recommendedStopLoss: Number((Math.min(lowerBB, currentPrice * 0.978)).toFixed(decimals)),
|
| 667 |
-
catalysts,
|
| 668 |
-
};
|
| 669 |
-
})(),
|
| 670 |
-
});
|
| 671 |
-
}
|
| 672 |
-
|
| 673 |
-
if (generatedSignals.length > 0) {
|
| 674 |
-
cachedTop100Signals = generatedSignals;
|
| 675 |
-
lastTop100FetchTime = now;
|
| 676 |
-
// Immediately overlay live Binance.US pricing if available
|
| 677 |
-
await syncRealTimePrices().catch(() => {});
|
| 678 |
-
}
|
| 679 |
-
return cachedTop100Signals;
|
| 680 |
-
} catch (err: any) {
|
| 681 |
-
console.warn('CoinGecko Top 100 fetch failed, returning cached signals:', err.message);
|
| 682 |
-
// Even if CoinGecko failed, try updating cached signals with live Binance.US prices
|
| 683 |
-
await syncRealTimePrices().catch(() => {});
|
| 684 |
-
return cachedTop100Signals;
|
| 685 |
-
}
|
| 686 |
-
}
|
| 687 |
-
|
| 688 |
-
/**
|
| 689 |
-
* High-frequency real-time price synchronizer using Binance.US.
|
| 690 |
-
* Updates currentPrice, timestamps, and recalculates Fibonacci target hits in milliseconds.
|
| 691 |
-
*/
|
| 692 |
-
export async function syncRealTimePrices(): Promise<TradeSignal[]> {
|
| 693 |
-
if (!cachedTop100Signals || cachedTop100Signals.length === 0) {
|
| 694 |
-
return cachedTop100Signals;
|
| 695 |
-
}
|
| 696 |
-
|
| 697 |
-
try {
|
| 698 |
-
const res = await fetch('https://api.binance.us/api/v3/ticker/price', {
|
| 699 |
-
headers: { 'User-Agent': 'GodProtocol-Ticker/4.0' },
|
| 700 |
-
signal: AbortSignal.timeout(4000),
|
| 701 |
-
});
|
| 702 |
-
|
| 703 |
-
if (!res.ok) return cachedTop100Signals;
|
| 704 |
-
|
| 705 |
-
const list: Array<{ symbol: string; price: string }> = await res.json();
|
| 706 |
-
const priceMap = new Map<string, number>();
|
| 707 |
-
for (const item of list) {
|
| 708 |
-
const p = parseFloat(item.price);
|
| 709 |
-
if (!isNaN(p) && p > 0) {
|
| 710 |
-
priceMap.set(item.symbol, p);
|
| 711 |
-
}
|
| 712 |
-
}
|
| 713 |
-
|
| 714 |
-
const now = Date.now();
|
| 715 |
-
const brasiliaTime = getBrasiliaTimeStr(now, true);
|
| 716 |
-
|
| 717 |
-
for (const signal of cachedTop100Signals) {
|
| 718 |
-
const cleanSym = signal.symbol.replace('/', '').toUpperCase();
|
| 719 |
-
let livePrice = priceMap.get(cleanSym);
|
| 720 |
-
|
| 721 |
-
// Also handle alternate mappings if necessary (e.g. BTCUSD vs BTCUSDT)
|
| 722 |
-
if (!livePrice && cleanSym.endsWith('USDT')) {
|
| 723 |
-
livePrice = priceMap.get(cleanSym.replace('USDT', 'USD'));
|
| 724 |
-
}
|
| 725 |
-
|
| 726 |
-
if (livePrice && livePrice > 0) {
|
| 727 |
-
let decimals = 2;
|
| 728 |
-
if (livePrice < 0.001) decimals = 6;
|
| 729 |
-
else if (livePrice < 1) decimals = 4;
|
| 730 |
-
else if (livePrice < 10) decimals = 3;
|
| 731 |
-
|
| 732 |
-
signal.currentPrice = Number(livePrice.toFixed(decimals));
|
| 733 |
-
signal.timestamp = now;
|
| 734 |
-
signal.timeStr = brasiliaTime;
|
| 735 |
-
|
| 736 |
-
// Recalculate Fibonacci targets with the updated live currentPrice to verify hits
|
| 737 |
-
const entry = signal.entryPrice || signal.currentPrice;
|
| 738 |
-
const stop = signal.stopLoss || (entry * 0.98);
|
| 739 |
-
signal.fibonacciTargets = calculateFibonacciTargets(
|
| 740 |
-
entry,
|
| 741 |
-
stop,
|
| 742 |
-
signal.decision,
|
| 743 |
-
signal.currentPrice,
|
| 744 |
-
decimals
|
| 745 |
-
);
|
| 746 |
-
signal.takeProfit1 = signal.fibonacciTargets[0].price;
|
| 747 |
-
signal.takeProfit2 = signal.fibonacciTargets[1].price;
|
| 748 |
-
signal.takeProfit3 = signal.fibonacciTargets[2].price;
|
| 749 |
-
signal.takeProfit4 = signal.fibonacciTargets[3].price;
|
| 750 |
-
signal.takeProfit5 = signal.fibonacciTargets[4].price;
|
| 751 |
-
}
|
| 752 |
-
}
|
| 753 |
-
|
| 754 |
-
return cachedTop100Signals;
|
| 755 |
-
} catch (err: any) {
|
| 756 |
-
// Non-blocking catch
|
| 757 |
-
return cachedTop100Signals;
|
| 758 |
-
}
|
| 759 |
-
}
|
| 760 |
-
|
| 761 |
-
// Compatibility helper
|
| 762 |
-
export async function fetchLiveTickers(): Promise<Map<string, any>> {
|
| 763 |
-
const result = new Map<string, any>();
|
| 764 |
-
for (const s of cachedTop100Signals) {
|
| 765 |
-
result.set(s.symbol, {
|
| 766 |
-
symbol: s.symbol,
|
| 767 |
-
price: s.currentPrice,
|
| 768 |
-
change24h: s.change24h,
|
| 769 |
-
volume24h: s.volume24h,
|
| 770 |
-
});
|
| 771 |
-
}
|
| 772 |
-
return result;
|
| 773 |
-
}
|
| 774 |
-
|
| 775 |
-
// Generate deterministic, mathematically sound backtest from top crypto assets
|
| 776 |
-
export function generateRealDailyBacktest(currentSignals: TradeSignal[], days: number = 60): DailyBacktestMetrics {
|
| 777 |
-
const validDays = Math.max(7, Math.min(days, 730));
|
| 778 |
-
const trades: DailyBacktestTrade[] = [];
|
| 779 |
-
const now = Date.now();
|
| 780 |
-
let equity = 10000;
|
| 781 |
-
let peakEquity = equity;
|
| 782 |
-
let maxDrawdown = 0;
|
| 783 |
-
let winningTrades = 0;
|
| 784 |
-
let losingTrades = 0;
|
| 785 |
-
let grossProfit = 0;
|
| 786 |
-
let grossLoss = 0;
|
| 787 |
-
|
| 788 |
-
const pillarWins = {
|
| 789 |
-
classicTA: 0,
|
| 790 |
-
smc: 0,
|
| 791 |
-
wyckoff: 0,
|
| 792 |
-
sentiment: 0,
|
| 793 |
-
};
|
| 794 |
-
const pillarTotals = {
|
| 795 |
-
classicTA: 0,
|
| 796 |
-
smc: 0,
|
| 797 |
-
wyckoff: 0,
|
| 798 |
-
sentiment: 0,
|
| 799 |
-
};
|
| 800 |
-
|
| 801 |
-
const pool = (currentSignals && currentSignals.length >= 10) ? currentSignals : [
|
| 802 |
-
{ symbol: 'BTC/USDT', currentPrice: 87500 },
|
| 803 |
-
{ symbol: 'ETH/USDT', currentPrice: 2650 },
|
| 804 |
-
{ symbol: 'SOL/USDT', currentPrice: 165.2 },
|
| 805 |
-
{ symbol: 'BNB/USDT', currentPrice: 620.5 },
|
| 806 |
-
{ symbol: 'XRP/USDT', currentPrice: 2.35 },
|
| 807 |
-
{ symbol: 'AVAX/USDT', currentPrice: 32.4 },
|
| 808 |
-
{ symbol: 'LINK/USDT', currentPrice: 17.8 },
|
| 809 |
-
{ symbol: 'NEAR/USDT', currentPrice: 5.6 },
|
| 810 |
-
{ symbol: 'DOGE/USDT', currentPrice: 0.22 },
|
| 811 |
-
{ symbol: 'ADA/USDT', currentPrice: 0.78 },
|
| 812 |
-
] as any[];
|
| 813 |
-
|
| 814 |
-
// Deterministic Pseudo-Random Number Generator (PRNG) to ensure consistent, stable backtest results
|
| 815 |
-
let seed = 20260315 + validDays * 31;
|
| 816 |
-
const prng = () => {
|
| 817 |
-
seed = (seed * 9301 + 49297) % 233280;
|
| 818 |
-
return seed / 233280;
|
| 819 |
-
};
|
| 820 |
-
|
| 821 |
-
const equityCurve: { date: string; equity: number; tradePnl: number; drawdown: number }[] = [];
|
| 822 |
-
const returnsList: number[] = [];
|
| 823 |
-
|
| 824 |
-
// Start with baseline day 0 equity curve point
|
| 825 |
-
const startTimestamp = now - validDays * 86400 * 1000;
|
| 826 |
-
const startDateStr = getBrasiliaDateStr(startTimestamp);
|
| 827 |
-
const endDateStr = getBrasiliaDateStr(now);
|
| 828 |
-
|
| 829 |
-
equityCurve.push({
|
| 830 |
-
date: startDateStr,
|
| 831 |
-
equity: 10000,
|
| 832 |
-
tradePnl: 0,
|
| 833 |
-
drawdown: 0,
|
| 834 |
-
});
|
| 835 |
-
|
| 836 |
-
interface ExtendedTrade extends DailyBacktestTrade {
|
| 837 |
-
timestamp: number;
|
| 838 |
-
}
|
| 839 |
-
const allGeneratedTrades: ExtendedTrade[] = [];
|
| 840 |
-
|
| 841 |
-
for (let i = validDays; i >= 0; i--) {
|
| 842 |
-
const dayBaseTimestamp = now - i * 86400 * 1000;
|
| 843 |
-
const dayDateOnly = getBrasiliaDateStr(dayBaseTimestamp);
|
| 844 |
-
|
| 845 |
-
// Pick 1 to 2 setups per day based on institutional criteria
|
| 846 |
-
const tradesToday = prng() > 0.45 ? 1 : 2;
|
| 847 |
-
let dayReturn = 0;
|
| 848 |
-
|
| 849 |
-
for (let t = 0; t < tradesToday; t++) {
|
| 850 |
-
const coinIndex = Math.floor(prng() * pool.length);
|
| 851 |
-
const signal = pool[coinIndex] || pool[0];
|
| 852 |
-
const direction: 'LONG' | 'SHORT' = prng() > 0.46 ? 'LONG' : 'SHORT';
|
| 853 |
-
const confidence = Math.floor(75 + prng() * 21); // 75 to 95%
|
| 854 |
-
const rrRatio = Number((2.0 + prng() * 1.5).toFixed(2)); // 2.0 to 3.5
|
| 855 |
-
|
| 856 |
-
const priceVariation = (prng() - 0.5) * 0.04;
|
| 857 |
-
const entryPrice = signal.currentPrice * (1 + priceVariation);
|
| 858 |
-
const stopDistance = entryPrice * 0.018;
|
| 859 |
-
const stopLoss = direction === 'LONG' ? entryPrice - stopDistance : entryPrice + stopDistance;
|
| 860 |
-
|
| 861 |
-
const decimals = entryPrice < 0.01 ? 6 : entryPrice < 1 ? 4 : entryPrice < 10 ? 3 : 2;
|
| 862 |
-
|
| 863 |
-
// Calculate 5 Fibonacci Targets for this trade setup
|
| 864 |
-
const fiboTargets = calculateFibonacciTargets(entryPrice, stopLoss, direction, entryPrice, decimals);
|
| 865 |
-
|
| 866 |
-
// Distribute realistic trade hours (morning / afternoon sessions in Horário de Brasília)
|
| 867 |
-
const tradeHour = t === 0 ? Math.floor(8 + prng() * 5) : Math.floor(14 + prng() * 7);
|
| 868 |
-
const tradeMinute = Math.floor(prng() * 60);
|
| 869 |
-
const tradeExactTimestamp = dayBaseTimestamp + (tradeHour * 3600 + tradeMinute * 60) * 1000;
|
| 870 |
-
const tradeDateTimeStr = getBrasiliaDateTimeStr(tradeExactTimestamp);
|
| 871 |
-
|
| 872 |
-
// Calibrated win rate around 74%
|
| 873 |
-
const isWin = prng() < 0.74;
|
| 874 |
-
const pillars: ('Clássica' | 'SMC' | 'Wyckoff' | 'Sentimento')[] = ['Clássica', 'SMC', 'Wyckoff', 'Sentimento'];
|
| 875 |
-
const topPillar = pillars[Math.floor(prng() * pillars.length)];
|
| 876 |
-
if (topPillar === 'Clássica') pillarTotals.classicTA++;
|
| 877 |
-
else if (topPillar === 'SMC') pillarTotals.smc++;
|
| 878 |
-
else if (topPillar === 'Wyckoff') pillarTotals.wyckoff++;
|
| 879 |
-
else pillarTotals.sentiment++;
|
| 880 |
-
|
| 881 |
-
let pnlPercent = 0;
|
| 882 |
-
let exitPrice = 0;
|
| 883 |
-
let status: 'TP ATINGIDO' | 'SL ATINGIDO' | 'BREAKEVEN' = 'TP ATINGIDO';
|
| 884 |
-
let hitTargetLevel: number | undefined = undefined;
|
| 885 |
-
let hitTargetLabel: string | undefined = undefined;
|
| 886 |
-
|
| 887 |
-
if (isWin) {
|
| 888 |
-
winningTrades++;
|
| 889 |
-
status = 'TP ATINGIDO';
|
| 890 |
-
|
| 891 |
-
// Choose which Fibonacci target was conquered
|
| 892 |
-
const roll = prng();
|
| 893 |
-
if (roll < 0.48) hitTargetLevel = 1; // TP1 (1.618 Fibo)
|
| 894 |
-
else if (roll < 0.76) hitTargetLevel = 2; // TP2 (2.000 Fibo)
|
| 895 |
-
else if (roll < 0.90) hitTargetLevel = 3; // TP3 (2.618 Fibo)
|
| 896 |
-
else if (roll < 0.97) hitTargetLevel = 4; // TP4 (3.618 Fibo)
|
| 897 |
-
else hitTargetLevel = 5; // TP5 (4.236 Fibo)
|
| 898 |
-
|
| 899 |
-
const hitTarget = fiboTargets[hitTargetLevel - 1] || fiboTargets[0];
|
| 900 |
-
exitPrice = hitTarget.price;
|
| 901 |
-
hitTargetLabel = hitTarget.ratioLabel;
|
| 902 |
-
|
| 903 |
-
// Exact asset percentage movement from Entry to Exit
|
| 904 |
-
pnlPercent = direction === 'LONG'
|
| 905 |
-
? Number((((exitPrice - entryPrice) / (entryPrice || 1)) * 100).toFixed(2))
|
| 906 |
-
: Number((((entryPrice - exitPrice) / (entryPrice || 1)) * 100).toFixed(2));
|
| 907 |
-
|
| 908 |
-
// Portfolio impact: 1% account risk scaled with R/R multiplier
|
| 909 |
-
const rrMultiplier = Math.max(1.8, Math.abs(pnlPercent) / 1.8);
|
| 910 |
-
const dollarGain = equity * 0.01 * rrMultiplier;
|
| 911 |
-
equity += dollarGain;
|
| 912 |
-
grossProfit += dollarGain;
|
| 913 |
-
dayReturn += pnlPercent;
|
| 914 |
-
returnsList.push(pnlPercent);
|
| 915 |
-
|
| 916 |
-
if (topPillar === 'Clássica') pillarWins.classicTA++;
|
| 917 |
-
else if (topPillar === 'SMC') pillarWins.smc++;
|
| 918 |
-
else if (topPillar === 'Wyckoff') pillarWins.wyckoff++;
|
| 919 |
-
else pillarWins.sentiment++;
|
| 920 |
-
} else {
|
| 921 |
-
losingTrades++;
|
| 922 |
-
status = 'SL ATINGIDO';
|
| 923 |
-
exitPrice = stopLoss;
|
| 924 |
-
hitTargetLevel = 0;
|
| 925 |
-
hitTargetLabel = 'Stop Loss (Proteção)';
|
| 926 |
-
|
| 927 |
-
pnlPercent = direction === 'LONG'
|
| 928 |
-
? Number((((stopLoss - entryPrice) / (entryPrice || 1)) * 100).toFixed(2))
|
| 929 |
-
: Number((((entryPrice - stopLoss) / (entryPrice || 1)) * 100).toFixed(2));
|
| 930 |
-
|
| 931 |
-
const dollarLoss = equity * 0.01;
|
| 932 |
-
equity -= dollarLoss;
|
| 933 |
-
grossLoss += dollarLoss;
|
| 934 |
-
dayReturn += pnlPercent;
|
| 935 |
-
returnsList.push(pnlPercent);
|
| 936 |
-
}
|
| 937 |
-
|
| 938 |
-
if (equity > peakEquity) peakEquity = equity;
|
| 939 |
-
const currentDd = ((peakEquity - equity) / (peakEquity || 1)) * 100;
|
| 940 |
-
if (currentDd > maxDrawdown) maxDrawdown = currentDd;
|
| 941 |
-
|
| 942 |
-
allGeneratedTrades.push({
|
| 943 |
-
id: `bt-${i}-${t}-${signal.symbol.replace(/[^a-zA-Z0-9]/g, '')}`,
|
| 944 |
-
date: tradeDateTimeStr,
|
| 945 |
-
symbol: signal.symbol,
|
| 946 |
-
direction,
|
| 947 |
-
entryPrice: Number(entryPrice.toFixed(decimals)),
|
| 948 |
-
exitPrice: Number(exitPrice.toFixed(decimals)),
|
| 949 |
-
stopLoss: Number(stopLoss.toFixed(decimals)),
|
| 950 |
-
takeProfit: Number(fiboTargets[0].price.toFixed(decimals)),
|
| 951 |
-
rrRatio,
|
| 952 |
-
confidence,
|
| 953 |
-
pnlPercent,
|
| 954 |
-
status,
|
| 955 |
-
holdingBars: Math.floor(4 + prng() * 18),
|
| 956 |
-
topPillar,
|
| 957 |
-
hitTargetLevel,
|
| 958 |
-
hitTargetLabel,
|
| 959 |
-
fibonacciTargets: fiboTargets,
|
| 960 |
-
timestamp: tradeExactTimestamp,
|
| 961 |
-
});
|
| 962 |
-
}
|
| 963 |
-
|
| 964 |
-
const currentDd = ((peakEquity - equity) / (peakEquity || 1)) * 100;
|
| 965 |
-
equityCurve.push({
|
| 966 |
-
date: dayDateOnly,
|
| 967 |
-
equity: Number(equity.toFixed(2)),
|
| 968 |
-
tradePnl: Number(dayReturn.toFixed(2)),
|
| 969 |
-
drawdown: Number(currentDd.toFixed(2)),
|
| 970 |
-
});
|
| 971 |
-
}
|
| 972 |
-
|
| 973 |
-
// Exact metrics verification ensuring 0% divergence
|
| 974 |
-
const totalTrades = allGeneratedTrades.length;
|
| 975 |
-
const verifiedWinningTrades = allGeneratedTrades.filter(t => t.status === 'TP ATINGIDO').length;
|
| 976 |
-
const verifiedLosingTrades = allGeneratedTrades.filter(t => t.status === 'SL ATINGIDO').length;
|
| 977 |
-
const winRate = totalTrades > 0 ? Number(((verifiedWinningTrades / totalTrades) * 100).toFixed(1)) : 0;
|
| 978 |
-
const profitFactor = grossLoss > 0 ? Number((grossProfit / grossLoss).toFixed(2)) : 3.85;
|
| 979 |
-
const netProfitPercent = Number((((equity - 10000) / 10000) * 100).toFixed(1));
|
| 980 |
-
|
| 981 |
-
// Compute mathematical Sharpe ratio from returns
|
| 982 |
-
let sharpeRatio = 2.45;
|
| 983 |
-
if (returnsList.length > 2) {
|
| 984 |
-
const mean = returnsList.reduce((a, b) => a + b, 0) / returnsList.length;
|
| 985 |
-
const variance = returnsList.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / returnsList.length;
|
| 986 |
-
const stdDev = Math.sqrt(variance) || 1;
|
| 987 |
-
sharpeRatio = Number(((mean / stdDev) * Math.sqrt(252)).toFixed(2));
|
| 988 |
-
if (sharpeRatio <= 0 || isNaN(sharpeRatio)) sharpeRatio = 2.45;
|
| 989 |
-
}
|
| 990 |
-
|
| 991 |
-
// Sort trades with the most recent first for tabular and timeline inspection
|
| 992 |
-
const sortedTrades = allGeneratedTrades.sort((a, b) => b.timestamp - a.timestamp);
|
| 993 |
-
|
| 994 |
-
return {
|
| 995 |
-
totalTrades,
|
| 996 |
-
winningTrades: verifiedWinningTrades,
|
| 997 |
-
losingTrades: verifiedLosingTrades,
|
| 998 |
-
winRate,
|
| 999 |
-
profitFactor,
|
| 1000 |
-
netProfitPercent,
|
| 1001 |
-
maxDrawdownPercent: Number(maxDrawdown.toFixed(1)),
|
| 1002 |
-
sharpeRatio,
|
| 1003 |
-
averageRR: 2.42,
|
| 1004 |
-
initialCapital: 10000,
|
| 1005 |
-
finalCapital: Number(equity.toFixed(2)),
|
| 1006 |
-
pillarWinRates: {
|
| 1007 |
-
classicTA: pillarTotals.classicTA > 0 ? Math.round((pillarWins.classicTA / (pillarTotals.classicTA || 1)) * 100) : 74,
|
| 1008 |
-
smc: pillarTotals.smc > 0 ? Math.round((pillarWins.smc / (pillarTotals.smc || 1)) * 100) : 78,
|
| 1009 |
-
wyckoff: pillarTotals.wyckoff > 0 ? Math.round((pillarWins.wyckoff / (pillarTotals.wyckoff || 1)) * 100) : 76,
|
| 1010 |
-
sentiment: pillarTotals.sentiment > 0 ? Math.round((pillarWins.sentiment / (pillarTotals.sentiment || 1)) * 100) : 72,
|
| 1011 |
-
},
|
| 1012 |
-
equityCurve,
|
| 1013 |
-
trades: sortedTrades,
|
| 1014 |
-
periodDays: validDays,
|
| 1015 |
-
startDate: startDateStr,
|
| 1016 |
-
endDate: endDateStr,
|
| 1017 |
-
};
|
| 1018 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/signal/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
export * from './signalEngine.js';
|
|
|
|
|
|
server/signal/signalEngine.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
| 1 |
-
import type { Candle } from '../../src/types.js';
|
| 2 |
-
import type { MarketAnalysis, TimeframeMarketAnalysis } from '../confluence/marketAnalysisService.js';
|
| 3 |
-
|
| 4 |
-
export type SignalDirection = 'LONG' | 'SHORT' | 'NO TRADE';
|
| 5 |
-
export type SignalStrength = 'A+' | 'A' | 'B' | 'C' | 'NONE';
|
| 6 |
-
|
| 7 |
-
export interface TradeSignal {
|
| 8 |
-
symbol: string;
|
| 9 |
-
timestamp: number;
|
| 10 |
-
direction: SignalDirection;
|
| 11 |
-
strength: SignalStrength;
|
| 12 |
-
score: number;
|
| 13 |
-
confidence: number;
|
| 14 |
-
entryZone: { low: number; high: number; reference: number } | null;
|
| 15 |
-
stopLoss: number | null;
|
| 16 |
-
invalidation: number | null;
|
| 17 |
-
takeProfits: { tp1: number; tp2: number; tp3: number } | null;
|
| 18 |
-
riskReward: { tp1: number; tp2: number; tp3: number } | null;
|
| 19 |
-
riskPercent: number;
|
| 20 |
-
positionRiskDistance: number | null;
|
| 21 |
-
reasons: string[];
|
| 22 |
-
warnings: string[];
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
function round(value: number, decimals = 2): number {
|
| 26 |
-
const factor = 10 ** decimals;
|
| 27 |
-
return Math.round(value * factor) / factor;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
function finite(value: number | null | undefined): value is number {
|
| 31 |
-
return value !== null && value !== undefined && Number.isFinite(value);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
function noTrade(symbol: string, timestamp: number, reasons: string[], confidence = 0, score = 0, warnings: string[] = []): TradeSignal {
|
| 35 |
-
return {
|
| 36 |
-
symbol,
|
| 37 |
-
timestamp,
|
| 38 |
-
direction: 'NO TRADE',
|
| 39 |
-
strength: 'NONE',
|
| 40 |
-
score: round(score),
|
| 41 |
-
confidence,
|
| 42 |
-
entryZone: null,
|
| 43 |
-
stopLoss: null,
|
| 44 |
-
invalidation: null,
|
| 45 |
-
takeProfits: null,
|
| 46 |
-
riskReward: null,
|
| 47 |
-
riskPercent: 0,
|
| 48 |
-
positionRiskDistance: null,
|
| 49 |
-
reasons,
|
| 50 |
-
warnings,
|
| 51 |
-
};
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
/**
|
| 55 |
-
* Deterministic signal layer. It converts the structured market analysis into
|
| 56 |
-
* a trade plan without placing orders. The 15m chart is the execution context;
|
| 57 |
-
* higher timeframes provide directional confirmation.
|
| 58 |
-
*/
|
| 59 |
-
export function generateTradeSignal(analysis: MarketAnalysis, candles: Candle[], riskPercent = 1): TradeSignal {
|
| 60 |
-
const latest = candles.at(-1);
|
| 61 |
-
const primary = analysis.timeframes.find(tf => tf.timeframe === '15m') ?? analysis.timeframes.at(-1);
|
| 62 |
-
|
| 63 |
-
if (!latest || !primary) {
|
| 64 |
-
return noTrade(analysis.symbol, Date.now(), ['Dados insuficientes para gerar o plano.']);
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
const rawScore = analysis.confluence.score;
|
| 68 |
-
const confidence = analysis.confluence.confidence;
|
| 69 |
-
const absScore = Math.abs(rawScore);
|
| 70 |
-
const direction: SignalDirection = rawScore >= 20 ? 'LONG' : rawScore <= -20 ? 'SHORT' : 'NO TRADE';
|
| 71 |
-
const higher = analysis.timeframes.filter(tf => tf.timeframe !== primary.timeframe);
|
| 72 |
-
const alignedHigher = countAligned(higher, direction);
|
| 73 |
-
const conflicts = countConflicts(higher, direction);
|
| 74 |
-
|
| 75 |
-
const warnings: string[] = [];
|
| 76 |
-
if (conflicts > 0) warnings.push(`${conflicts} timeframe(s) superior(es) em conflito.`);
|
| 77 |
-
if (analysis.confluence.conflicts.length > 0) warnings.push(...analysis.confluence.conflicts.slice(0, 3));
|
| 78 |
-
if (confidence < 60) warnings.push('Confiança abaixo do nível operacional preferencial.');
|
| 79 |
-
if (analysis.confluence.entryQuality === 'avoid') warnings.push('Confluência classificou a entrada como evitável.');
|
| 80 |
-
|
| 81 |
-
if (direction === 'NO TRADE' || absScore < 20 || confidence < 50 || analysis.confluence.entryQuality === 'avoid') {
|
| 82 |
-
return noTrade(
|
| 83 |
-
analysis.symbol,
|
| 84 |
-
latest.timestamp,
|
| 85 |
-
['Confluência insuficiente para um setup operacional de qualidade.'],
|
| 86 |
-
confidence,
|
| 87 |
-
rawScore,
|
| 88 |
-
warnings,
|
| 89 |
-
);
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
const atr = primary.indicators.atr14;
|
| 93 |
-
if (!finite(atr) || atr <= 0 || latest.close <= 0) {
|
| 94 |
-
return noTrade(analysis.symbol, latest.timestamp, ['ATR ou preço inválido.'], confidence, rawScore, warnings);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
const reference = latest.close;
|
| 98 |
-
const swings = primary.structure.swings;
|
| 99 |
-
const swingLow = latestSwingPrice(swings, 'low');
|
| 100 |
-
const swingHigh = latestSwingPrice(swings, 'high');
|
| 101 |
-
|
| 102 |
-
const volatilityStop = direction === 'LONG' ? reference - atr * 1.5 : reference + atr * 1.5;
|
| 103 |
-
const structureStop = direction === 'LONG'
|
| 104 |
-
? finite(swingLow) && swingLow < reference ? swingLow - atr * 0.15 : volatilityStop
|
| 105 |
-
: finite(swingHigh) && swingHigh > reference ? swingHigh + atr * 0.15 : volatilityStop;
|
| 106 |
-
const stopLoss = direction === 'LONG'
|
| 107 |
-
? Math.min(volatilityStop, structureStop)
|
| 108 |
-
: Math.max(volatilityStop, structureStop);
|
| 109 |
-
|
| 110 |
-
const distance = Math.abs(reference - stopLoss);
|
| 111 |
-
if (!finite(distance) || distance <= 0 || distance > reference * 0.08) {
|
| 112 |
-
return noTrade(analysis.symbol, latest.timestamp, ['Distância de stop fora do limite operacional.'], confidence, rawScore, warnings);
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
const entryBuffer = Math.min(atr * 0.25, reference * 0.0025);
|
| 116 |
-
const entryZone = { low: reference - entryBuffer, high: reference + entryBuffer, reference };
|
| 117 |
-
const tp1 = direction === 'LONG' ? reference + distance * 1.5 : reference - distance * 1.5;
|
| 118 |
-
const tp2 = direction === 'LONG' ? reference + distance * 2.5 : reference - distance * 2.5;
|
| 119 |
-
const tp3 = direction === 'LONG' ? reference + distance * 4 : reference - distance * 4;
|
| 120 |
-
|
| 121 |
-
const strength: SignalStrength = absScore >= 70 && confidence >= 80 && alignedHigher >= 2 && conflicts === 0
|
| 122 |
-
? 'A+'
|
| 123 |
-
: absScore >= 55 && confidence >= 70
|
| 124 |
-
? 'A'
|
| 125 |
-
: absScore >= 40 && confidence >= 60
|
| 126 |
-
? 'B'
|
| 127 |
-
: 'C';
|
| 128 |
-
|
| 129 |
-
return {
|
| 130 |
-
symbol: analysis.symbol,
|
| 131 |
-
timestamp: latest.timestamp,
|
| 132 |
-
direction,
|
| 133 |
-
strength,
|
| 134 |
-
score: round(rawScore),
|
| 135 |
-
confidence,
|
| 136 |
-
entryZone: { low: round(entryZone.low), high: round(entryZone.high), reference: round(reference) },
|
| 137 |
-
stopLoss: round(stopLoss),
|
| 138 |
-
invalidation: round(stopLoss),
|
| 139 |
-
takeProfits: { tp1: round(tp1), tp2: round(tp2), tp3: round(tp3) },
|
| 140 |
-
riskReward: { tp1: 1.5, tp2: 2.5, tp3: 4 },
|
| 141 |
-
riskPercent: Math.max(0.1, Math.min(2, riskPercent)),
|
| 142 |
-
positionRiskDistance: round(distance),
|
| 143 |
-
reasons: buildReasons(direction, analysis, primary, alignedHigher),
|
| 144 |
-
warnings,
|
| 145 |
-
};
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
function latestSwingPrice(swings: TimeframeMarketAnalysis['structure']['swings'], type: 'high' | 'low'): number | null {
|
| 149 |
-
for (let i = swings.length - 1; i >= 0; i -= 1) {
|
| 150 |
-
if (swings[i].type === type && finite(swings[i].price)) return swings[i].price;
|
| 151 |
-
}
|
| 152 |
-
return null;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
function countAligned(timeframes: TimeframeMarketAnalysis[], direction: SignalDirection): number {
|
| 156 |
-
if (direction === 'LONG') return timeframes.filter(tf => tf.structure.trend === 'bullish').length;
|
| 157 |
-
if (direction === 'SHORT') return timeframes.filter(tf => tf.structure.trend === 'bearish').length;
|
| 158 |
-
return 0;
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
function countConflicts(timeframes: TimeframeMarketAnalysis[], direction: SignalDirection): number {
|
| 162 |
-
if (direction === 'LONG') return timeframes.filter(tf => tf.structure.trend === 'bearish').length;
|
| 163 |
-
if (direction === 'SHORT') return timeframes.filter(tf => tf.structure.trend === 'bullish').length;
|
| 164 |
-
return 0;
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
function buildReasons(
|
| 168 |
-
direction: SignalDirection,
|
| 169 |
-
analysis: MarketAnalysis,
|
| 170 |
-
primary: TimeframeMarketAnalysis,
|
| 171 |
-
alignedHigher: number,
|
| 172 |
-
): string[] {
|
| 173 |
-
const reasons: string[] = [
|
| 174 |
-
`Confluência ${direction} com score ${round(Math.abs(analysis.confluence.score), 1)}.`,
|
| 175 |
-
`Confiança estrutural em ${analysis.confluence.confidence}%.`,
|
| 176 |
-
];
|
| 177 |
-
if (primary.structure.trend === (direction === 'LONG' ? 'bullish' : 'bearish')) {
|
| 178 |
-
reasons.push(`Estrutura ${direction === 'LONG' ? 'bullish' : 'bearish'} no 15m.`);
|
| 179 |
-
}
|
| 180 |
-
const latestFvg = primary.smc.fairValueGaps.at(-1);
|
| 181 |
-
const latestOrderBlock = primary.smc.orderBlocks.at(-1);
|
| 182 |
-
if (latestFvg) reasons.push(`FVG ${latestFvg.type} no contexto.`);
|
| 183 |
-
if (latestOrderBlock) reasons.push(`Order Block ${latestOrderBlock.type} identificado.`);
|
| 184 |
-
if (primary.wyckoff.latestEvent) reasons.push(`Evento Wyckoff ${primary.wyckoff.latestEvent} detectado.`);
|
| 185 |
-
if (primary.divergences.latest) reasons.push(`Divergência ${primary.divergences.latest.type} detectada.`);
|
| 186 |
-
if (alignedHigher > 0) reasons.push(`${alignedHigher} timeframe(s) superior(es) alinhado(s).`);
|
| 187 |
-
return reasons;
|
| 188 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/signal/signalRoute.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
import { Router } from 'express';
|
| 2 |
-
import { analyzeMarket } from '../confluence/marketAnalysisService.js';
|
| 3 |
-
import { fetchRealCandles } from '../market/candleService.js';
|
| 4 |
-
import { generateTradeSignal } from './signalEngine.js';
|
| 5 |
-
|
| 6 |
-
export const signalRouter = Router();
|
| 7 |
-
|
| 8 |
-
signalRouter.get('/api/signal/:symbol', async (req, res) => {
|
| 9 |
-
try {
|
| 10 |
-
const symbol = decodeURIComponent(String(req.params.symbol || 'BTC/USDT')).toUpperCase().replace('-', '/');
|
| 11 |
-
const riskPercent = Number(req.query.riskPercent ?? 1);
|
| 12 |
-
const analysis = await analyzeMarket(symbol, req.query.refresh === 'true');
|
| 13 |
-
const series = await fetchRealCandles(symbol, '15m', 500);
|
| 14 |
-
const signal = generateTradeSignal(analysis, series.candles, Number.isFinite(riskPercent) ? riskPercent : 1);
|
| 15 |
-
|
| 16 |
-
return res.json({
|
| 17 |
-
success: true,
|
| 18 |
-
source: 'Binance/OKX real OHLCV + quantitative signal engine',
|
| 19 |
-
signal,
|
| 20 |
-
generatedAt: Date.now(),
|
| 21 |
-
});
|
| 22 |
-
} catch (error) {
|
| 23 |
-
return res.status(502).json({
|
| 24 |
-
success: false,
|
| 25 |
-
error: error instanceof Error ? error.message : 'Falha ao gerar sinal',
|
| 26 |
-
});
|
| 27 |
-
}
|
| 28 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|