diff --git a/.gitignore b/.gitignore index e3881a2..806e43e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ __pycache__/ *.pyc .pytest_cache/ portfolio_state.json +node_modules/ +dist/ +.DS_Store diff --git a/index.html b/index.html new file mode 100644 index 0000000..215a6b9 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + Portfolio Monte Carlo Simulator + + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d9f0d56 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2689 @@ +{ + "name": "portfolio-simulator", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "portfolio-simulator", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.12.7" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "gh-pages": "^6.1.1", + "vite": "^5.4.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1aef01c --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "portfolio-simulator", + "version": "1.0.0", + "description": "Monte Carlo Portfolio Simulator with actual and simulated returns", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "deploy": "vite build && gh-pages -d dist" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.12.7" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "gh-pages": "^6.1.1", + "vite": "^5.4.2" + } +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..8345aac --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,209 @@ +import React, { useState, useCallback } from 'react'; +import Header from './components/Header.jsx'; +import Sidebar, { TABS } from './components/Sidebar.jsx'; +import PortfolioBuilder from './components/PortfolioBuilder.jsx'; +import SimulationConfig from './components/SimulationConfig.jsx'; +import ResultsDashboard from './components/ResultsDashboard.jsx'; +import ActualReturnsPanel from './components/ActualReturnsPanel.jsx'; +import { runMonteCarlo, runBacktest } from './engine/simulation.js'; +import { fetchAllReturns } from './data/fetchReturns.js'; + +function App() { + const [activeTab, setActiveTab] = useState('portfolio'); + const [isSimulating, setIsSimulating] = useState(false); + const [isFetchingReturns, setIsFetchingReturns] = useState(false); + + // Portfolio state + const [selectedAssets, setSelectedAssets] = useState([]); + + // Simulation config + const [simConfig, setSimConfig] = useState({ + mode: 'simulated', + nPaths: 1200, + nYears: 20, + seed: 42, + regimeWeights: { standard: 70, inflation: 20, liquidity: 10 }, + }); + + // Results + const [simResults, setSimResults] = useState(null); + + // Actual returns data + const [returnData, setReturnData] = useState({}); + const [returnMetadata, setReturnMetadata] = useState({}); + const [returnErrors, setReturnErrors] = useState({}); + + // Header stats (from most recent simulation) + const headerStats = simResults && !simResults.error + ? simConfig.mode === 'simulated' + ? { + cagr: simResults.summary?.cagr?.median, + vol: simResults.summary?.vol?.median, + maxDD: simResults.summary?.maxDD?.median, + sharpe: simResults.summary?.sharpe?.median, + } + : simResults.stats + ? { + cagr: simResults.stats.cagr, + vol: simResults.stats.vol, + maxDD: simResults.stats.maxDD, + sharpe: simResults.stats.sharpe, + } + : null + : null; + + // ─── Run Simulation ─── + const handleRunSimulation = useCallback(async () => { + if (selectedAssets.length === 0) return; + setIsSimulating(true); + setSimResults(null); + + // Use setTimeout to allow UI to update before heavy computation + await new Promise((resolve) => setTimeout(resolve, 50)); + + try { + if (simConfig.mode === 'simulated') { + // Normalize regime weights to 0-1 + const totalRegimeWeight = Object.values(simConfig.regimeWeights).reduce((s, v) => s + v, 0); + const normalizedRegimes = {}; + for (const [k, v] of Object.entries(simConfig.regimeWeights)) { + normalizedRegimes[k] = totalRegimeWeight > 0 ? v / totalRegimeWeight : 0; + } + + const results = runMonteCarlo({ + assets: selectedAssets, + nPaths: simConfig.nPaths, + nYears: simConfig.nYears, + regimeWeights: normalizedRegimes, + seed: simConfig.seed, + }); + setSimResults(results); + } else { + // Actual or Hybrid mode - need to fetch returns first + let currentReturnData = returnData; + if (Object.keys(currentReturnData).length === 0) { + setIsFetchingReturns(true); + const { data, metadata, errors } = await fetchAllReturns(selectedAssets); + currentReturnData = data; + setReturnData(data); + setReturnMetadata(metadata); + setReturnErrors(errors); + setIsFetchingReturns(false); + } + + const totalRegimeWeight = Object.values(simConfig.regimeWeights).reduce((s, v) => s + v, 0); + const normalizedRegimes = {}; + for (const [k, v] of Object.entries(simConfig.regimeWeights)) { + normalizedRegimes[k] = totalRegimeWeight > 0 ? v / totalRegimeWeight : 0; + } + + const results = runBacktest({ + assets: selectedAssets, + returnData: currentReturnData, + fillMissing: simConfig.mode === 'hybrid', + regimeWeights: normalizedRegimes, + }); + setSimResults(results); + } + + setActiveTab('results'); + } catch (err) { + console.error('Simulation error:', err); + setSimResults({ error: err.message }); + setActiveTab('results'); + } finally { + setIsSimulating(false); + } + }, [selectedAssets, simConfig, returnData]); + + // ─── Fetch Returns ─── + const handleFetchReturns = useCallback(async () => { + if (selectedAssets.length === 0) return; + setIsFetchingReturns(true); + try { + const { data, metadata, errors } = await fetchAllReturns(selectedAssets); + setReturnData(data); + setReturnMetadata(metadata); + setReturnErrors(errors); + } catch (err) { + console.error('Fetch error:', err); + } finally { + setIsFetchingReturns(false); + } + }, [selectedAssets]); + + // ─── Render Tab Content ─── + const renderTabContent = () => { + switch (activeTab) { + case 'portfolio': + return ( + + ); + case 'settings': + return ( + + ); + case 'results': + return ( + + ); + case 'historical': + return ( + + ); + default: + return null; + } + }; + + return ( +
+
+ +
+ + +
+
+ {renderTabContent()} +
+
+
+ + {/* Mobile tab bar */} +
+ {TABS.map((tab) => ( + + ))} +
+
+ ); +} + +export default App; diff --git a/src/components/ActualReturnsPanel.jsx b/src/components/ActualReturnsPanel.jsx new file mode 100644 index 0000000..8a74048 --- /dev/null +++ b/src/components/ActualReturnsPanel.jsx @@ -0,0 +1,305 @@ +import React, { useState, useMemo } from 'react'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, + ResponsiveContainer, Legend, +} from 'recharts'; + +const COLORS = [ + '#0f1419', '#1d9bf0', '#f4212e', '#00ba7c', '#f59e0b', + '#7856ff', '#c026d3', '#14b8a6', '#ff7a00', '#1e40af', +]; + +function ActualReturnsPanel({ + selectedAssets, + returnData, + metadata, + errors, + onFetchReturns, + isFetching, +}) { + const [showCumulative, setShowCumulative] = useState(true); + + const hasData = returnData && Object.keys(returnData).length > 0; + + // Build chart data + const chartData = useMemo(() => { + if (!hasData) return []; + + // Find common dates + const allDates = new Set(); + for (const data of Object.values(returnData)) { + if (data?.dates) { + for (const d of data.dates) allDates.add(d); + } + } + const sortedDates = Array.from(allDates).sort(); + if (sortedDates.length === 0) return []; + + if (showCumulative) { + // Cumulative return chart + const cumulative = {}; + for (const [ticker, data] of Object.entries(returnData)) { + if (!data?.dates) continue; + cumulative[ticker] = { value: 1.0, dateMap: {} }; + for (let i = 0; i < data.dates.length; i++) { + cumulative[ticker].value *= 1 + data.returns[i]; + cumulative[ticker].dateMap[data.dates[i]] = cumulative[ticker].value; + } + } + + return sortedDates.map((date) => { + const point = { date }; + for (const [ticker, cum] of Object.entries(cumulative)) { + point[ticker] = cum.dateMap[date] || null; + } + return point; + }); + } else { + // Monthly return chart + return sortedDates.map((date) => { + const point = { date }; + for (const [ticker, data] of Object.entries(returnData)) { + if (!data?.dates) continue; + const idx = data.dates.indexOf(date); + point[ticker] = idx >= 0 ? data.returns[idx] * 100 : null; + } + return point; + }); + } + }, [returnData, showCumulative, hasData]); + + // Compute per-asset stats + const assetStats = useMemo(() => { + const stats = {}; + for (const [ticker, data] of Object.entries(returnData || {})) { + if (!data?.returns || data.returns.length === 0) continue; + const rets = data.returns; + const nMonths = rets.length; + const nYears = nMonths / 12; + const cumReturn = rets.reduce((acc, r) => acc * (1 + r), 1); + const cagr = nYears > 0 ? (Math.pow(cumReturn, 1 / nYears) - 1) * 100 : 0; + + const mean = rets.reduce((s, r) => s + r, 0) / nMonths; + const variance = rets.reduce((s, r) => s + (r - mean) ** 2, 0) / (nMonths - 1); + const vol = Math.sqrt(variance) * Math.sqrt(12) * 100; + + let peak = 1; + let maxDD = 0; + let val = 1; + for (const r of rets) { + val *= 1 + r; + if (val > peak) peak = val; + const dd = (peak - val) / peak; + if (dd > maxDD) maxDD = dd; + } + + stats[ticker] = { + cagr, + vol, + maxDD: maxDD * 100, + cumReturn, + nMonths, + nYears: nYears.toFixed(1), + startDate: data.dates[0], + endDate: data.dates[data.dates.length - 1], + }; + } + return stats; + }, [returnData]); + + const tickers = Object.keys(returnData || {}); + + return ( +
+
+
+
Historical Returns
+
Actual return data fetched from Yahoo Finance
+
+
+ +
+
+ + {/* Data Availability */} + {(metadata || errors) && ( +
+
Data Status
+
+ {selectedAssets.map((asset) => { + const ticker = asset.ticker || asset.id; + const meta = metadata?.[ticker]; + const error = errors?.[ticker]; + const data = returnData?.[ticker]; + + return ( +
+
+ {ticker} + {asset.name} +
+
+ {data && ( + + {data.startDate} to {data.endDate} ({data.totalMonths} mo) + + )} + {meta?.source === 'comparable' && ( + + Proxy: {meta.comparableTicker} + + )} + {data && meta?.source !== 'comparable' && ( + OK + )} + {error && !data && ( + + Error + + )} + {!data && !error && !isFetching && ( + + Not fetched + + )} +
+
+ ); + })} +
+
+ )} + + {/* Chart */} + {hasData && chartData.length > 0 && ( +
+
+
+ {showCumulative ? 'Cumulative Returns' : 'Monthly Returns'} +
+
+ + +
+
+ + + + + + showCumulative ? `${v.toFixed(1)}x` : `${v.toFixed(0)}%` + } + fontSize={11} + tick={{ fill: '#65676b' }} + /> + [ + showCumulative ? `${value?.toFixed(3)}x` : `${value?.toFixed(2)}%`, + name, + ]} + contentStyle={{ + background: 'white', + border: '1px solid rgba(0,0,0,0.15)', + borderRadius: 8, + fontSize: 12, + fontFamily: 'var(--font-mono)', + }} + /> + + {tickers.map((ticker, i) => ( + + ))} + + +
+ )} + + {/* Per-Asset Stats Table */} + {Object.keys(assetStats).length > 0 && ( +
+
Individual Asset Statistics
+
+ + + + + + + + + + + + + + {Object.entries(assetStats).map(([ticker, s]) => ( + + + + + + + + + + ))} + +
AssetPeriodMonthsCAGRVolatilityMax DDTotal Return
{ticker}{s.startDate} to {s.endDate}{s.nMonths}= 0 ? 'var(--status-positive)' : 'var(--status-negative)' }}> + {s.cagr >= 0 ? '+' : ''}{s.cagr.toFixed(1)}% + {s.vol.toFixed(1)}%-{s.maxDD.toFixed(1)}%{s.cumReturn.toFixed(2)}x
+
+
+ )} + + {!hasData && !isFetching && ( +
+
📜
+

+ {selectedAssets.length === 0 + ? 'Add assets in the Portfolio tab, then fetch returns here' + : 'Click "Fetch Returns" to load historical data from Yahoo Finance'} +

+
+ )} +
+ ); +} + +export default ActualReturnsPanel; diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..b1b3724 --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,67 @@ +import React from 'react'; + +function Header({ stats, isSimulating }) { + const formatPct = (v) => { + if (v == null) return '--'; + return `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`; + }; + + const getColor = (v, invert = false) => { + if (v == null) return 'var(--text-muted)'; + const isGood = invert ? v < 0 : v > 0; + return isGood ? 'var(--status-positive)' : v === 0 ? 'var(--text-muted)' : 'var(--status-negative)'; + }; + + return ( +
+
+
+
+ + + +
+
Portfolio Simulator
+
+ + {stats && ( +
+
+ + {formatPct(stats.cagr)} + + Med CAGR +
+
+ + {stats.vol != null ? `${stats.vol.toFixed(1)}%` : '--'} + + Vol +
+
+ + {stats.maxDD != null ? `-${stats.maxDD.toFixed(1)}%` : '--'} + + Max DD +
+
+ + {stats.sharpe != null ? stats.sharpe.toFixed(2) : '--'} + + Sharpe +
+
+ )} +
+ +
+
+ + {isSimulating ? 'Simulating...' : 'Ready'} +
+
+
+ ); +} + +export default Header; diff --git a/src/components/PortfolioBuilder.jsx b/src/components/PortfolioBuilder.jsx new file mode 100644 index 0000000..727b716 --- /dev/null +++ b/src/components/PortfolioBuilder.jsx @@ -0,0 +1,222 @@ +import React, { useState, useMemo } from 'react'; +import { ASSETS, ASSET_CATEGORIES, searchAssets, PRESET_PORTFOLIOS, getAsset } from '../data/assets.js'; + +function PortfolioBuilder({ selectedAssets, onAssetsChange }) { + const [searchQuery, setSearchQuery] = useState(''); + const [showPresets, setShowPresets] = useState(false); + + const filteredAssets = useMemo(() => searchAssets(searchQuery), [searchQuery]); + const selectedIds = new Set(selectedAssets.map((a) => a.id)); + const totalWeight = selectedAssets.reduce((sum, a) => sum + a.weight, 0); + + const addAsset = (asset) => { + if (selectedIds.has(asset.id)) return; + onAssetsChange([...selectedAssets, { ...asset, weight: 0 }]); + }; + + const removeAsset = (id) => { + onAssetsChange(selectedAssets.filter((a) => a.id !== id)); + }; + + const updateWeight = (id, weight) => { + const w = Math.max(0, Math.min(100, parseFloat(weight) || 0)); + onAssetsChange(selectedAssets.map((a) => (a.id === id ? { ...a, weight: w } : a))); + }; + + const loadPreset = (presetKey) => { + const preset = PRESET_PORTFOLIOS[presetKey]; + if (!preset) return; + const newAssets = preset.assets.map((pa) => { + const def = getAsset(pa.id); + return def ? { ...def, weight: pa.weight } : null; + }).filter(Boolean); + onAssetsChange(newAssets); + setShowPresets(false); + }; + + const equalizeWeights = () => { + if (selectedAssets.length === 0) return; + const w = Math.round((100 / selectedAssets.length) * 100) / 100; + onAssetsChange(selectedAssets.map((a) => ({ ...a, weight: w }))); + }; + + // Group filtered assets by category + const groupedAssets = useMemo(() => { + const groups = {}; + for (const asset of filteredAssets) { + const cat = asset.category || 'other'; + if (!groups[cat]) groups[cat] = []; + groups[cat].push(asset); + } + return groups; + }, [filteredAssets]); + + return ( +
+
+
+
Portfolio Builder
+
Select assets and assign weights for your portfolio
+
+
+ + {selectedAssets.length > 0 && ( + + )} +
+
+ + {/* Preset Portfolios */} + {showPresets && ( +
+
Preset Portfolios
+
+ {Object.entries(PRESET_PORTFOLIOS).map(([key, preset]) => ( + + ))} +
+
+ )} + +
+ {/* Asset Search */} +
+
Available Assets
+ setSearchQuery(e.target.value)} + style={{ marginBottom: 'var(--space-sm)' }} + /> +
+ {Object.entries(groupedAssets).map(([category, assets]) => ( + +
+ {ASSET_CATEGORIES[category] || category} +
+ {assets.map((asset) => ( + + ))} +
+ ))} +
+
+ + {/* Selected Assets & Weights */} +
+
+
Portfolio Weights
+ {selectedAssets.length > 0 && ( + + )} +
+ + {selectedAssets.length === 0 ? ( +
+
+
+

+ Select assets from the left panel or load a preset +

+
+ ) : ( + <> +
+ {selectedAssets.map((asset) => ( +
+ {asset.ticker || asset.id} + {asset.name} + updateWeight(asset.id, e.target.value)} + /> + updateWeight(asset.id, e.target.value)} + /> + % + +
+ ))} +
+ +
+ Total Weight + 100 ? 'over' : 'under' + }`}> + {totalWeight.toFixed(1)}% + +
+ + {totalWeight > 100.1 && ( +
+ Portfolio is over-allocated (leveraged). This is intentional for return-stacking strategies. +
+ )} + {totalWeight < 99.9 && totalWeight > 0 && ( +
+ Portfolio is under-allocated. Remaining {(100 - totalWeight).toFixed(1)}% will be held as cash. +
+ )} + + )} +
+
+
+ ); +} + +export default PortfolioBuilder; diff --git a/src/components/ResultsDashboard.jsx b/src/components/ResultsDashboard.jsx new file mode 100644 index 0000000..ecbf596 --- /dev/null +++ b/src/components/ResultsDashboard.jsx @@ -0,0 +1,541 @@ +import React, { useMemo } from 'react'; +import { + AreaChart, Area, LineChart, Line, BarChart, Bar, + XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + ReferenceLine, +} from 'recharts'; + +function ResultsDashboard({ results, mode }) { + const isBacktest = mode === 'actual' || mode === 'hybrid'; + + if (!results) { + return ( +
+
+
+
Results
+
Run a simulation to see results
+
+
+
+
📈
+

Configure your portfolio and run a simulation from the Settings tab

+
+
+ ); + } + + if (results.error) { + return ( +
+
+
+
Results
+
Error
+
+
+
+ {results.error} +
+
+ ); + } + + // Monte Carlo results + if (!isBacktest && results.percentilePaths) { + return ; + } + + // Backtest results + if (isBacktest && results.path) { + return ; + } + + return null; +} + +function MonteCarloResults({ results }) { + const { percentilePaths, summary, nPaths, nYears, nMonths } = results; + + // Build fan chart data with banded areas for proper percentile rendering + const fanData = useMemo(() => { + const data = []; + const step = Math.max(1, Math.floor(nMonths / 240)); + for (let m = 0; m <= nMonths; m += step) { + const yr = m / 12; + const p5 = percentilePaths.p5[m]; + const p10 = percentilePaths.p10[m]; + const p25 = percentilePaths.p25[m]; + const p50 = percentilePaths.p50[m]; + const p75 = percentilePaths.p75[m]; + const p90 = percentilePaths.p90[m]; + const p95 = percentilePaths.p95[m]; + data.push({ + year: yr, + // Stacked band components + base: p5, + band_5_10: Math.max(0, p10 - p5), + band_10_25: Math.max(0, p25 - p10), + band_25_50: Math.max(0, p50 - p25), + band_50_75: Math.max(0, p75 - p50), + band_75_90: Math.max(0, p90 - p75), + band_90_95: Math.max(0, p95 - p90), + // Raw values for tooltip and median line + p5, p10, p25, p50, p75, p90, p95, + }); + } + // Always include final month + const lastYr = data[data.length - 1]?.year; + if (lastYr !== nYears) { + const p5 = percentilePaths.p5[nMonths]; + const p10 = percentilePaths.p10[nMonths]; + const p25 = percentilePaths.p25[nMonths]; + const p50 = percentilePaths.p50[nMonths]; + const p75 = percentilePaths.p75[nMonths]; + const p90 = percentilePaths.p90[nMonths]; + const p95 = percentilePaths.p95[nMonths]; + data.push({ + year: nYears, + base: p5, + band_5_10: Math.max(0, p10 - p5), + band_10_25: Math.max(0, p25 - p10), + band_25_50: Math.max(0, p50 - p25), + band_50_75: Math.max(0, p75 - p50), + band_75_90: Math.max(0, p90 - p75), + band_90_95: Math.max(0, p95 - p90), + p5, p10, p25, p50, p75, p90, p95, + }); + } + return data; + }, [percentilePaths, nMonths, nYears]); + + // Build distribution data for terminal wealth + const distData = useMemo(() => { + const cagrs = []; + for (let p = 0; p < results.paths.length; p++) { + const fm = results.paths[p][nMonths]; + if (fm > 0) { + cagrs.push((Math.pow(fm, 1 / nYears) - 1) * 100); + } + } + cagrs.sort((a, b) => a - b); + + // Create histogram bins + const min = Math.floor(cagrs[0] || -10); + const max = Math.ceil(cagrs[cagrs.length - 1] || 30); + const nBins = 40; + const binWidth = (max - min) / nBins; + const bins = []; + for (let i = 0; i < nBins; i++) { + const lo = min + i * binWidth; + const hi = lo + binWidth; + const count = cagrs.filter((c) => c >= lo && c < hi).length; + bins.push({ + range: `${lo.toFixed(0)}%`, + value: lo + binWidth / 2, + count, + pct: (count / cagrs.length) * 100, + }); + } + return bins; + }, [results.paths, nMonths, nYears]); + + const fmt = (v, d = 1) => (v != null ? v.toFixed(d) : '--'); + const fmtPct = (v, d = 1) => (v != null ? `${v >= 0 ? '+' : ''}${v.toFixed(d)}%` : '--'); + + return ( +
+
+
+
Monte Carlo Results
+
+ {nPaths.toLocaleString()} paths · {nYears} year horizon +
+
+
+ + {/* Summary Stats */} +
+
+
= 0 ? 'var(--status-positive)' : 'var(--status-negative)' }}> + {fmtPct(summary.cagr.median)} +
+
Median CAGR
+
5th: {fmtPct(summary.cagr.p5)} · 95th: {fmtPct(summary.cagr.p95)}
+
+
+
{fmt(summary.vol.median)}%
+
Median Volatility
+
5th: {fmt(summary.vol.p5)}% · 95th: {fmt(summary.vol.p95)}%
+
+
+
+ -{fmt(summary.maxDD.median)}% +
+
Avg Max Drawdown
+
Best 5%: -{fmt(summary.maxDD.p5)}% · Worst 5%: -{fmt(summary.maxDD.p95)}%
+
+
+
{fmt(summary.sharpe.median, 2)}
+
Median Sharpe
+
5th: {fmt(summary.sharpe.p5, 2)} · 95th: {fmt(summary.sharpe.p95, 2)}
+
+
+
{fmt(summary.sortino.median, 2)}
+
Median Sortino
+
5th: {fmt(summary.sortino.p5, 2)} · 95th: {fmt(summary.sortino.p95, 2)}
+
+
+
{fmt(summary.finalMult.median, 2)}x
+
Median Final Wealth
+
5th: {fmt(summary.finalMult.p5, 2)}x · 95th: {fmt(summary.finalMult.p95, 2)}x
+
+
+ + {/* Fan Chart */} +
+
Portfolio Growth - Percentile Fan Chart
+
+
5th-95th
+
10th-90th
+
25th-75th
+
Median
+
+ + + + `${v}y`} + fontSize={11} + tick={{ fill: '#65676b' }} + /> + `${v.toFixed(1)}x`} + fontSize={11} + tick={{ fill: '#65676b' }} + domain={['auto', 'auto']} + /> + { + if (!active || !payload || !payload.length) return null; + const d = payload[0]?.payload; + if (!d) return null; + return ( +
+
Year {label}
+
95th: {d.p95?.toFixed(2)}x
+
90th: {d.p90?.toFixed(2)}x
+
75th: {d.p75?.toFixed(2)}x
+
Median: {d.p50?.toFixed(2)}x
+
25th: {d.p25?.toFixed(2)}x
+
10th: {d.p10?.toFixed(2)}x
+
5th: {d.p5?.toFixed(2)}x
+
+ ); + }} + /> + + {/* Stacked bands: invisible base + colored bands */} + + + + + + + + {/* Median line on top */} + +
+
+
+ + {/* CAGR Distribution */} +
+
CAGR Distribution
+ + + + + `${v.toFixed(0)}%`} + fontSize={10} + tick={{ fill: '#65676b' }} + /> + [`${value.toFixed(1)}%`, 'Frequency']} + contentStyle={{ + background: 'white', + border: '1px solid rgba(0,0,0,0.15)', + borderRadius: 8, + fontSize: 12, + }} + /> + + + + +
+ + {/* Percentile Table */} +
+
Detailed Percentile Breakdown
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metric5th10th25thMedian75th90th95th
CAGR{fmtPct(summary.cagr.p5)}{fmtPct(summary.cagr.p10)}{fmtPct(summary.cagr.p25)}{fmtPct(summary.cagr.median)}{fmtPct(summary.cagr.p75)}{fmtPct(summary.cagr.p90)}{fmtPct(summary.cagr.p95)}
Volatility{fmt(summary.vol.p5)}%{fmt(summary.vol.p10)}%{fmt(summary.vol.p25)}%{fmt(summary.vol.median)}%{fmt(summary.vol.p75)}%{fmt(summary.vol.p90)}%{fmt(summary.vol.p95)}%
Max Drawdown-{fmt(summary.maxDD.p5)}%-{fmt(summary.maxDD.p10)}%-{fmt(summary.maxDD.p25)}%-{fmt(summary.maxDD.median)}%-{fmt(summary.maxDD.p75)}%-{fmt(summary.maxDD.p90)}%-{fmt(summary.maxDD.p95)}%
Sharpe Ratio{fmt(summary.sharpe.p5, 2)}{fmt(summary.sharpe.p10, 2)}{fmt(summary.sharpe.p25, 2)}{fmt(summary.sharpe.median, 2)}{fmt(summary.sharpe.p75, 2)}{fmt(summary.sharpe.p90, 2)}{fmt(summary.sharpe.p95, 2)}
Sortino Ratio{fmt(summary.sortino.p5, 2)}{fmt(summary.sortino.p10, 2)}{fmt(summary.sortino.p25, 2)}{fmt(summary.sortino.median, 2)}{fmt(summary.sortino.p75, 2)}{fmt(summary.sortino.p90, 2)}{fmt(summary.sortino.p95, 2)}
Final Wealth{fmt(summary.finalMult.p5, 2)}x{fmt(summary.finalMult.p10, 2)}x{fmt(summary.finalMult.p25, 2)}x{fmt(summary.finalMult.median, 2)}x{fmt(summary.finalMult.p75, 2)}x{fmt(summary.finalMult.p90, 2)}x{fmt(summary.finalMult.p95, 2)}x
+
+
+ ); +} + +function BacktestResults({ results }) { + const { path, dates, stats, dataAvailability, monthlyReturns } = results; + + const chartData = useMemo(() => { + const data = []; + const step = Math.max(1, Math.floor(path.length / 300)); + for (let i = 0; i < path.length; i += step) { + data.push({ + date: dates[i] || `M${i}`, + value: path[i], + }); + } + if (data[data.length - 1]?.date !== dates[dates.length - 1]) { + data.push({ + date: dates[dates.length - 1], + value: path[path.length - 1], + }); + } + return data; + }, [path, dates]); + + // Monthly return distribution + const drawdownData = useMemo(() => { + const data = []; + let peak = path[0]; + const step = Math.max(1, Math.floor(path.length / 300)); + for (let i = 0; i < path.length; i += step) { + if (path[i] > peak) peak = path[i]; + const dd = peak > 0 ? ((path[i] - peak) / peak) * 100 : 0; + data.push({ + date: dates[i] || `M${i}`, + drawdown: dd, + }); + } + return data; + }, [path, dates]); + + const fmt = (v, d = 1) => (v != null ? v.toFixed(d) : '--'); + + return ( +
+
+
+
Historical Backtest Results
+
+ {stats.startDate} to {stats.endDate} · {stats.nYears} years +
+
+
+ + {/* Stats */} +
+
+
= 0 ? 'var(--status-positive)' : 'var(--status-negative)' }}> + {stats.cagr >= 0 ? '+' : ''}{fmt(stats.cagr)}% +
+
CAGR
+
+
+
{fmt(stats.vol)}%
+
Volatility
+
+
+
-{fmt(stats.maxDD)}%
+
Max Drawdown
+
+
+
{fmt(stats.sharpe, 2)}
+
Sharpe Ratio
+
+
+
{fmt(stats.sortino, 2)}
+
Sortino Ratio
+
+
+
{fmt(stats.finalMult, 2)}x
+
Total Return
+
+
+ + {/* Cumulative Return Chart */} +
+
Cumulative Portfolio Growth
+ + + + + `${v.toFixed(1)}x`} + fontSize={11} + tick={{ fill: '#65676b' }} + /> + [`${value.toFixed(3)}x`, 'Portfolio Value']} + contentStyle={{ + background: 'white', + border: '1px solid rgba(0,0,0,0.15)', + borderRadius: 8, + fontSize: 12, + fontFamily: 'var(--font-mono)', + }} + /> + + + + +
+ + {/* Drawdown Chart */} +
+
Drawdown
+ + + + + `${v.toFixed(0)}%`} + fontSize={11} + tick={{ fill: '#65676b' }} + /> + [`${value.toFixed(1)}%`, 'Drawdown']} + contentStyle={{ + background: 'white', + border: '1px solid rgba(0,0,0,0.15)', + borderRadius: 8, + fontSize: 12, + }} + /> + + + +
+ + {/* Data Availability */} + {dataAvailability && Object.keys(dataAvailability).length > 0 && ( +
+
Data Availability
+
+ {Object.entries(dataAvailability).map(([ticker, info]) => ( +
+ {ticker} + + {info.available + ? `${info.startDate} to ${info.endDate} (${info.totalMonths} months)` + : 'No data available'} + + + {info.available ? 'OK' : 'Missing'} + +
+ ))} +
+
+ )} +
+ ); +} + +export default ResultsDashboard; diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx new file mode 100644 index 0000000..78f5260 --- /dev/null +++ b/src/components/Sidebar.jsx @@ -0,0 +1,40 @@ +import React from 'react'; + +const TABS = [ + { id: 'portfolio', icon: '\u{1F4CA}', label: 'Portfolio' }, + { id: 'settings', icon: '\u{2699}\uFE0F', label: 'Settings' }, + { id: 'results', icon: '\u{1F4C8}', label: 'Results' }, + { id: 'historical', icon: '\u{1F4DC}', label: 'Historical' }, +]; + +function Sidebar({ activeTab, onTabChange }) { + return ( + + ); +} + +export { TABS }; +export default Sidebar; diff --git a/src/components/SimulationConfig.jsx b/src/components/SimulationConfig.jsx new file mode 100644 index 0000000..776e55d --- /dev/null +++ b/src/components/SimulationConfig.jsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { CRISIS_REGIMES } from '../engine/simulation.js'; + +function SimulationConfig({ + config, + onConfigChange, + onRunSimulation, + isSimulating, + selectedAssets, +}) { + const totalWeight = selectedAssets.reduce((sum, a) => sum + a.weight, 0); + const canRun = selectedAssets.length > 0 && totalWeight > 0 && !isSimulating; + + const updateConfig = (key, value) => { + onConfigChange({ ...config, [key]: value }); + }; + + const updateRegime = (regime, value) => { + const val = parseFloat(value) || 0; + const newWeights = { ...config.regimeWeights, [regime]: val }; + + // Normalize: adjust other weights proportionally + const others = Object.keys(newWeights).filter((k) => k !== regime); + const otherSum = others.reduce((s, k) => s + newWeights[k], 0); + const remaining = Math.max(0, 100 - val); + + if (otherSum > 0) { + for (const k of others) { + newWeights[k] = (newWeights[k] / otherSum) * remaining; + } + } else { + // Distribute equally + for (const k of others) { + newWeights[k] = remaining / others.length; + } + } + + updateConfig('regimeWeights', newWeights); + }; + + return ( +
+
+
+
Simulation Settings
+
Configure Monte Carlo parameters and crash regimes
+
+
+ +
+ {/* Simulation Parameters */} +
+
Parameters
+ +
+ Return Mode +
+ + + +
+
+ + {config.mode === 'simulated' && ( + <> +
+ Simulation Paths +
+ updateConfig('nPaths', parseInt(e.target.value))} + style={{ width: 100, accentColor: 'var(--text-primary)' }} + /> + {config.nPaths} +
+
+ +
+ Time Horizon (years) +
+ updateConfig('nYears', parseInt(e.target.value))} + style={{ width: 100, accentColor: 'var(--text-primary)' }} + /> + {config.nYears}yr +
+
+ +
+ Random Seed + updateConfig('seed', parseInt(e.target.value) || 42)} + style={{ width: 70, textAlign: 'right' }} + /> +
+ + )} + + {config.mode === 'actual' && ( +
+

+ Uses historical return data from Yahoo Finance. Portfolio will be backtested over the common + available date range. +

+
+ )} + + {config.mode === 'hybrid' && ( + <> +
+

+ Uses actual returns where available, with simulated fills for assets with shorter history. + Simulated fills are conditioned on other assets' actual performance. +

+
+
+ Simulation Paths (for fills) +
+ updateConfig('nPaths', parseInt(e.target.value))} + style={{ width: 100, accentColor: 'var(--text-primary)' }} + /> + {config.nPaths} +
+
+ + )} +
+ + {/* Crash Regimes */} +
+
Crash Regime Mix
+

+ Define the probability-weighted mix of crisis behaviors. These affect how assets correlate + and perform during market crashes. +

+ +
+ {Object.entries(CRISIS_REGIMES).map(([key, regime]) => { + const pct = config.regimeWeights[key] || 0; + return ( +
+
+ {regime.label} + updateRegime(key, e.target.value)} + /> + {Math.round(pct)}% +
+
+ {regime.description} +
+
+ ); + })} +
+
+
+ + {/* Run Button */} + + + {!canRun && !isSimulating && selectedAssets.length === 0 && ( +

+ Add assets in the Portfolio tab first +

+ )} +
+ ); +} + +export default SimulationConfig; diff --git a/src/data/assets.js b/src/data/assets.js new file mode 100644 index 0000000..2d9b1c2 --- /dev/null +++ b/src/data/assets.js @@ -0,0 +1,539 @@ +/** + * Asset Definitions + * + * Each asset maps to a base class model for simulation, + * plus metadata for display, data fetching, and comparable assets. + */ + +export const ASSET_CATEGORIES = { + us_equity: 'US Equity', + intl_equity: 'International Equity', + bonds: 'Bonds', + alternatives: 'Alternatives', + crypto: 'Crypto', + leveraged: 'Leveraged', + return_stacked: 'Return Stacked', + managed_futures: 'Managed Futures', + cash: 'Cash / Short-Term', +}; + +export const ASSETS = [ + // ─── US Equity ─── + { + id: 'SPY', + ticker: 'SPY', + name: 'S&P 500 ETF', + category: 'us_equity', + baseClass: 'us_equity', + expenseRatio: 0.0009, + inceptionDate: '1993-01-29', + comparable: null, + description: 'Tracks the S&P 500 index', + }, + { + id: 'QQQ', + ticker: 'QQQ', + name: 'Nasdaq 100 ETF', + category: 'us_equity', + baseClass: 'us_tech', + expenseRatio: 0.002, + inceptionDate: '1999-03-10', + comparable: null, + description: 'Tracks the Nasdaq-100 index', + }, + { + id: 'IWM', + ticker: 'IWM', + name: 'Russell 2000 ETF', + category: 'us_equity', + baseClass: 'us_small_cap', + expenseRatio: 0.0019, + inceptionDate: '2000-05-22', + comparable: null, + description: 'Tracks US small-cap stocks', + }, + { + id: 'VTI', + ticker: 'VTI', + name: 'Total US Stock Market', + category: 'us_equity', + baseClass: 'us_equity', + expenseRatio: 0.0003, + inceptionDate: '2001-05-24', + comparable: 'SPY', + description: 'Total US stock market exposure', + }, + { + id: 'VOO', + ticker: 'VOO', + name: 'Vanguard S&P 500', + category: 'us_equity', + baseClass: 'us_equity', + expenseRatio: 0.0003, + inceptionDate: '2010-09-07', + comparable: 'SPY', + description: 'Vanguard S&P 500 tracker', + }, + + // ─── International Equity ─── + { + id: 'EFA', + ticker: 'EFA', + name: 'MSCI EAFE ETF', + category: 'intl_equity', + baseClass: 'intl_developed', + expenseRatio: 0.0032, + inceptionDate: '2001-08-14', + comparable: null, + description: 'Developed international markets ex-US & Canada', + }, + { + id: 'VWO', + ticker: 'VWO', + name: 'Emerging Markets ETF', + category: 'intl_equity', + baseClass: 'intl_emerging', + expenseRatio: 0.0008, + inceptionDate: '2005-03-04', + comparable: 'EEM', + description: 'FTSE Emerging Markets exposure', + }, + { + id: 'EEM', + ticker: 'EEM', + name: 'iShares Emerging Markets', + category: 'intl_equity', + baseClass: 'intl_emerging', + expenseRatio: 0.0068, + inceptionDate: '2003-04-07', + comparable: null, + description: 'MSCI Emerging Markets index', + }, + { + id: 'VXUS', + ticker: 'VXUS', + name: 'Total International Stock', + category: 'intl_equity', + baseClass: 'intl_developed', + expenseRatio: 0.0007, + inceptionDate: '2011-01-26', + comparable: 'EFA', + description: 'Total international stock market', + }, + + // ─── Bonds ─── + { + id: 'AGG', + ticker: 'AGG', + name: 'US Aggregate Bond', + category: 'bonds', + baseClass: 'us_aggregate_bond', + expenseRatio: 0.0003, + inceptionDate: '2003-09-22', + comparable: null, + description: 'US investment-grade bond market', + }, + { + id: 'TLT', + ticker: 'TLT', + name: '20+ Year Treasury', + category: 'bonds', + baseClass: 'us_long_treasury', + expenseRatio: 0.0015, + inceptionDate: '2002-07-22', + comparable: null, + description: 'Long-term US Treasury bonds', + }, + { + id: 'IEF', + ticker: 'IEF', + name: '7-10 Year Treasury', + category: 'bonds', + baseClass: 'us_aggregate_bond', + expenseRatio: 0.0015, + inceptionDate: '2002-07-22', + comparable: null, + description: 'Intermediate-term US Treasury bonds', + }, + { + id: 'TIP', + ticker: 'TIP', + name: 'TIPS Bond ETF', + category: 'bonds', + baseClass: 'tips', + expenseRatio: 0.0019, + inceptionDate: '2003-12-04', + comparable: null, + description: 'Treasury Inflation-Protected Securities', + }, + { + id: 'SHY', + ticker: 'SHY', + name: '1-3 Year Treasury', + category: 'bonds', + baseClass: 'cash', + expenseRatio: 0.0015, + inceptionDate: '2002-07-22', + comparable: null, + description: 'Short-term US Treasury bonds', + }, + { + id: 'BND', + ticker: 'BND', + name: 'Total Bond Market', + category: 'bonds', + baseClass: 'us_aggregate_bond', + expenseRatio: 0.0003, + inceptionDate: '2007-04-03', + comparable: 'AGG', + description: 'Vanguard total bond market', + }, + + // ─── Alternatives ─── + { + id: 'GLD', + ticker: 'GLD', + name: 'Gold ETF', + category: 'alternatives', + baseClass: 'gold', + expenseRatio: 0.004, + inceptionDate: '2004-11-18', + comparable: null, + description: 'Physical gold price tracker', + }, + { + id: 'IAU', + ticker: 'IAU', + name: 'iShares Gold Trust', + category: 'alternatives', + baseClass: 'gold', + expenseRatio: 0.0025, + inceptionDate: '2005-01-21', + comparable: 'GLD', + description: 'Physical gold price tracker', + }, + { + id: 'DBC', + ticker: 'DBC', + name: 'Commodity Index ETF', + category: 'alternatives', + baseClass: 'commodities', + expenseRatio: 0.0087, + inceptionDate: '2006-02-03', + comparable: null, + description: 'Diversified commodity futures', + }, + { + id: 'VNQ', + ticker: 'VNQ', + name: 'Real Estate ETF', + category: 'alternatives', + baseClass: 'real_estate', + expenseRatio: 0.0012, + inceptionDate: '2004-09-23', + comparable: null, + description: 'US real estate investment trusts', + }, + + // ─── Crypto ─── + { + id: 'BTC', + ticker: 'BTC-USD', + name: 'Bitcoin', + category: 'crypto', + baseClass: 'crypto_major', + expenseRatio: 0, + inceptionDate: '2014-09-17', + comparable: null, + description: 'Bitcoin cryptocurrency', + }, + { + id: 'ETH', + ticker: 'ETH-USD', + name: 'Ethereum', + category: 'crypto', + baseClass: 'crypto_alt', + expenseRatio: 0, + inceptionDate: '2017-11-09', + comparable: 'BTC-USD', + description: 'Ethereum cryptocurrency', + }, + { + id: 'IBIT', + ticker: 'IBIT', + name: 'iShares Bitcoin Trust', + category: 'crypto', + baseClass: 'crypto_major', + expenseRatio: 0.0025, + inceptionDate: '2024-01-11', + comparable: 'BTC-USD', + description: 'Spot Bitcoin ETF', + }, + + // ─── Leveraged ─── + { + id: 'UPRO', + ticker: 'UPRO', + name: '3x S&P 500', + category: 'leveraged', + baseClass: null, + leverage: 3, + underlying: 'us_equity', + expenseRatio: 0.009, + inceptionDate: '2009-06-25', + comparable: null, + description: '3x daily leveraged S&P 500', + }, + { + id: 'TQQQ', + ticker: 'TQQQ', + name: '3x Nasdaq 100', + category: 'leveraged', + baseClass: null, + leverage: 3, + underlying: 'us_tech', + expenseRatio: 0.0086, + inceptionDate: '2010-02-09', + comparable: null, + description: '3x daily leveraged Nasdaq-100', + }, + { + id: 'SSO', + ticker: 'SSO', + name: '2x S&P 500', + category: 'leveraged', + baseClass: null, + leverage: 2, + underlying: 'us_equity', + expenseRatio: 0.0089, + inceptionDate: '2006-06-19', + comparable: null, + description: '2x daily leveraged S&P 500', + }, + { + id: 'QLD', + ticker: 'QLD', + name: '2x Nasdaq 100', + category: 'leveraged', + baseClass: null, + leverage: 2, + underlying: 'us_tech', + expenseRatio: 0.0095, + inceptionDate: '2006-06-19', + comparable: null, + description: '2x daily leveraged Nasdaq-100', + }, + + // ─── Return Stacked ─── + { + id: 'RSST', + ticker: 'RSST', + name: 'Return Stacked US Equity & Trend', + category: 'return_stacked', + baseClass: null, + stackedComponents: ['us_equity', 'managed_futures'], + expenseRatio: 0.0104, + inceptionDate: '2023-09-27', + comparable: null, + description: '100% US equity + 100% trend-following overlay', + }, + { + id: 'RSSB', + ticker: 'RSSB', + name: 'Return Stacked Global Equity & Bonds', + category: 'return_stacked', + baseClass: null, + stackedComponents: ['us_equity', 'us_aggregate_bond'], + expenseRatio: 0.011, + inceptionDate: '2023-09-27', + comparable: null, + description: '100% global equity + 100% bond overlay', + }, + { + id: 'RSBT', + ticker: 'RSBT', + name: 'Return Stacked Bonds & Trend', + category: 'return_stacked', + baseClass: null, + stackedComponents: ['us_aggregate_bond', 'managed_futures'], + expenseRatio: 0.0104, + inceptionDate: '2023-09-27', + comparable: null, + description: '100% bonds + 100% trend-following overlay', + }, + + // ─── Managed Futures ─── + { + id: 'DBMF', + ticker: 'DBMF', + name: 'Managed Futures Strategy', + category: 'managed_futures', + baseClass: 'managed_futures', + expenseRatio: 0.0085, + inceptionDate: '2019-05-08', + comparable: null, + description: 'Dynamic Beta managed futures replication', + }, + { + id: 'CTA', + ticker: 'CTA', + name: 'Simplify Managed Futures', + category: 'managed_futures', + baseClass: 'managed_futures', + expenseRatio: 0.0075, + inceptionDate: '2022-03-01', + comparable: 'DBMF', + description: 'Simplify managed futures strategy', + }, + { + id: 'KMLM', + ticker: 'KMLM', + name: 'KFA Mount Lucas Managed Futures', + category: 'managed_futures', + baseClass: 'managed_futures', + expenseRatio: 0.009, + inceptionDate: '2020-12-02', + comparable: 'DBMF', + description: 'Multi-asset trend following', + }, + + // ─── Cash / Short-Term ─── + { + id: 'BIL', + ticker: 'BIL', + name: 'T-Bills ETF', + category: 'cash', + baseClass: 'cash', + expenseRatio: 0.0014, + inceptionDate: '2007-05-25', + comparable: null, + description: '1-3 month US Treasury bills', + }, + { + id: 'SHV', + ticker: 'SHV', + name: 'Short Treasury ETF', + category: 'cash', + baseClass: 'cash', + expenseRatio: 0.0015, + inceptionDate: '2007-01-05', + comparable: null, + description: 'Short-term US Treasury bonds', + }, + { + id: 'SGOV', + ticker: 'SGOV', + name: '0-3 Month Treasury', + category: 'cash', + baseClass: 'cash', + expenseRatio: 0.0007, + inceptionDate: '2020-05-26', + comparable: 'BIL', + description: 'Ultra-short Treasury bills', + }, +]; + +/** + * Get an asset definition by ID or ticker + */ +export function getAsset(idOrTicker) { + return ASSETS.find((a) => a.id === idOrTicker || a.ticker === idOrTicker) || null; +} + +/** + * Search assets by query string (name, ticker, or category) + */ +export function searchAssets(query) { + if (!query) return ASSETS; + const q = query.toLowerCase(); + return ASSETS.filter( + (a) => + a.ticker.toLowerCase().includes(q) || + a.name.toLowerCase().includes(q) || + a.description.toLowerCase().includes(q) || + (ASSET_CATEGORIES[a.category] || '').toLowerCase().includes(q) + ); +} + +/** + * Create a custom asset definition for an unknown ticker + */ +export function createCustomAsset(ticker, params = {}) { + return { + id: ticker, + ticker: ticker, + name: params.name || ticker, + category: params.category || 'us_equity', + baseClass: params.baseClass || 'us_equity', + expenseRatio: params.expenseRatio || 0.005, + inceptionDate: params.inceptionDate || '2020-01-01', + comparable: params.comparable || 'SPY', + description: params.description || `Custom asset: ${ticker}`, + isCustom: true, + }; +} + +/** + * Get preset portfolio allocations + */ +export const PRESET_PORTFOLIOS = { + classic_60_40: { + name: 'Classic 60/40', + description: '60% US stocks, 40% US bonds', + assets: [ + { id: 'SPY', weight: 60 }, + { id: 'AGG', weight: 40 }, + ], + }, + all_weather: { + name: 'All Weather', + description: 'Ray Dalio-inspired risk parity', + assets: [ + { id: 'SPY', weight: 30 }, + { id: 'TLT', weight: 40 }, + { id: 'GLD', weight: 7.5 }, + { id: 'DBC', weight: 7.5 }, + { id: 'IEF', weight: 15 }, + ], + }, + return_stacked: { + name: 'Return Stacked', + description: 'Leveraged return stacking strategy', + assets: [ + { id: 'UPRO', weight: 40 }, + { id: 'RSST', weight: 28 }, + { id: 'RSSB', weight: 25 }, + { id: 'GLD', weight: 2 }, + { id: 'BTC', weight: 2 }, + { id: 'BIL', weight: 3 }, + ], + }, + aggressive_growth: { + name: 'Aggressive Growth', + description: 'High-risk growth portfolio', + assets: [ + { id: 'QQQ', weight: 40 }, + { id: 'SPY', weight: 25 }, + { id: 'IWM', weight: 10 }, + { id: 'VWO', weight: 10 }, + { id: 'BTC', weight: 10 }, + { id: 'GLD', weight: 5 }, + ], + }, + market_only: { + name: 'S&P 500 Only', + description: '100% S&P 500 benchmark', + assets: [{ id: 'SPY', weight: 100 }], + }, + golden_butterfly: { + name: 'Golden Butterfly', + description: 'Modified permanent portfolio', + assets: [ + { id: 'SPY', weight: 20 }, + { id: 'IWM', weight: 20 }, + { id: 'TLT', weight: 20 }, + { id: 'SHY', weight: 20 }, + { id: 'GLD', weight: 20 }, + ], + }, +}; diff --git a/src/data/fetchReturns.js b/src/data/fetchReturns.js new file mode 100644 index 0000000..dce8258 --- /dev/null +++ b/src/data/fetchReturns.js @@ -0,0 +1,200 @@ +/** + * Historical Return Data Fetcher + * + * Fetches monthly price data from Yahoo Finance via CORS proxies, + * computes monthly returns, and handles comparable asset fallbacks. + */ + +const CORS_PROXIES = [ + (url) => `https://corsproxy.io/?${encodeURIComponent(url)}`, + (url) => `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`, +]; + +/** + * Fetch monthly price data from Yahoo Finance + * @param {string} ticker - Yahoo Finance ticker symbol + * @param {string} startDate - Start date (YYYY-MM-DD) or null for max + * @returns {Object} {dates: string[], prices: number[], returns: number[]} + */ +export async function fetchMonthlyReturns(ticker, startDate = null) { + const period1 = startDate + ? Math.floor(new Date(startDate).getTime() / 1000) + : 0; + const period2 = Math.floor(Date.now() / 1000); + + const baseUrl = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?period1=${period1}&period2=${period2}&interval=1mo`; + + let data = null; + let lastError = null; + + // Try direct first, then CORS proxies + const urls = [baseUrl, ...CORS_PROXIES.map((proxy) => proxy(baseUrl))]; + + for (const url of urls) { + try { + const response = await fetch(url, { + headers: { Accept: 'application/json' }, + }); + if (!response.ok) continue; + data = await response.json(); + if (data?.chart?.result?.[0]) break; + data = null; + } catch (e) { + lastError = e; + continue; + } + } + + if (!data?.chart?.result?.[0]) { + throw new Error( + `Failed to fetch data for ${ticker}: ${lastError?.message || 'No data returned'}` + ); + } + + const result = data.chart.result[0]; + const timestamps = result.timestamp || []; + const adjClose = + result.indicators?.adjclose?.[0]?.adjclose || + result.indicators?.quote?.[0]?.close || + []; + + if (timestamps.length < 2 || adjClose.length < 2) { + throw new Error(`Insufficient data for ${ticker}`); + } + + // Convert to monthly returns + const dates = []; + const prices = []; + const returns = []; + + for (let i = 0; i < timestamps.length; i++) { + const price = adjClose[i]; + if (price == null || isNaN(price) || price <= 0) continue; + + const date = new Date(timestamps[i] * 1000); + const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + + prices.push(price); + dates.push(dateStr); + + if (prices.length >= 2) { + const prevPrice = prices[prices.length - 2]; + returns.push((price - prevPrice) / prevPrice); + } + } + + // Returns array is 1 shorter than dates/prices (first month has no return) + return { + ticker, + dates: dates.slice(1), // Align with returns + prices: prices.slice(1), + returns, + startDate: dates[1] || dates[0], + endDate: dates[dates.length - 1], + totalMonths: returns.length, + }; +} + +/** + * Fetch returns for multiple assets, handling comparable fallbacks + * @param {Array} assets - Asset definitions from assets.js + * @returns {Object} {[ticker]: {dates, returns, ...}, metadata: {}} + */ +export async function fetchAllReturns(assets) { + const results = {}; + const metadata = {}; + const errors = {}; + + // First pass: try to fetch each asset + const fetchPromises = assets.map(async (asset) => { + try { + const data = await fetchMonthlyReturns(asset.ticker); + results[asset.ticker] = data; + metadata[asset.ticker] = { + source: 'direct', + dataStart: data.startDate, + dataEnd: data.endDate, + totalMonths: data.totalMonths, + }; + } catch (e) { + errors[asset.ticker] = e.message; + + // Try comparable asset + if (asset.comparable) { + try { + const compData = await fetchMonthlyReturns(asset.comparable); + results[asset.ticker] = compData; + metadata[asset.ticker] = { + source: 'comparable', + comparableTicker: asset.comparable, + dataStart: compData.startDate, + dataEnd: compData.endDate, + totalMonths: compData.totalMonths, + note: `Using ${asset.comparable} as comparable proxy for ${asset.ticker}`, + }; + } catch (e2) { + errors[asset.ticker] = `${e.message}; comparable (${asset.comparable}) also failed: ${e2.message}`; + } + } + } + }); + + await Promise.all(fetchPromises); + + return { data: results, metadata, errors }; +} + +/** + * Analyze data availability across multiple assets + * @param {Object} returnData - {[ticker]: {dates, returns}} + * @returns {Object} availability analysis + */ +export function analyzeDataAvailability(returnData) { + const tickers = Object.keys(returnData); + if (tickers.length === 0) return { commonStart: null, commonEnd: null, gaps: {} }; + + let latestStart = '0000-00'; + let earliestEnd = '9999-99'; + const startDates = {}; + const endDates = {}; + + for (const ticker of tickers) { + const data = returnData[ticker]; + if (!data || !data.dates || data.dates.length === 0) continue; + startDates[ticker] = data.dates[0]; + endDates[ticker] = data.dates[data.dates.length - 1]; + if (data.dates[0] > latestStart) latestStart = data.dates[0]; + if (data.dates[data.dates.length - 1] < earliestEnd) + earliestEnd = data.dates[data.dates.length - 1]; + } + + // Identify assets with shorter history + const gaps = {}; + for (const ticker of tickers) { + if (startDates[ticker] && startDates[ticker] > latestStart.slice(0, 7)) { + // This is actually fine, latestStart is the constraint + } + if (startDates[ticker] && startDates[ticker] !== latestStart) { + gaps[ticker] = { + hasGap: startDates[ticker] > Object.values(startDates).sort()[0], + assetStart: startDates[ticker], + earliestAvailable: Object.values(startDates).sort()[0], + missingMonths: monthDiff(Object.values(startDates).sort()[0], startDates[ticker]), + }; + } + } + + return { + commonStart: latestStart, + commonEnd: earliestEnd, + startDates, + endDates, + gaps, + }; +} + +function monthDiff(dateA, dateB) { + const [yA, mA] = dateA.split('-').map(Number); + const [yB, mB] = dateB.split('-').map(Number); + return (yB - yA) * 12 + (mB - mA); +} diff --git a/src/engine/simulation.js b/src/engine/simulation.js new file mode 100644 index 0000000..7ea86ee --- /dev/null +++ b/src/engine/simulation.js @@ -0,0 +1,768 @@ +/** + * Monte Carlo Portfolio Simulation Engine + * + * Generates correlated fat-tailed return paths with: + * - Student-t distributions for fat tails + * - Volatility clustering (GARCH-like dynamics) + * - Regime-dependent crisis behavior + * - Leveraged product modeling (with beta slippage) + * - Return-stacked product modeling (RSST, RSSB) + * - Conditional simulation for filling missing history + */ + +// ─── Seedable PRNG (Mulberry32) ─── +function mulberry32(seed) { + return function () { + seed |= 0; + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +// ─── Random Distributions ─── +function randn(rng) { + let u, v, s; + do { + u = rng() * 2 - 1; + v = rng() * 2 - 1; + s = u * u + v * v; + } while (s >= 1 || s === 0); + return u * Math.sqrt((-2 * Math.log(s)) / s); +} + +function gammaVariate(rng, shape) { + // Marsaglia and Tsang's method for shape >= 1 + if (shape < 1) { + return gammaVariate(rng, shape + 1) * Math.pow(rng(), 1 / shape); + } + const d = shape - 1 / 3; + const c = 1 / Math.sqrt(9 * d); + while (true) { + let x, v; + do { + x = randn(rng); + v = 1 + c * x; + } while (v <= 0); + v = v * v * v; + const u = rng(); + if (u < 1 - 0.0331 * (x * x) * (x * x)) return d * v; + if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v; + } +} + +function studentT(rng, df) { + if (df == null || df > 60) return randn(rng); + const z = randn(rng); + const chi2 = gammaVariate(rng, df / 2) * 2; + const scaleFactor = Math.sqrt((df - 2) / df); + return (z / Math.sqrt(chi2 / df)) * scaleFactor; +} + +// ─── Cholesky Decomposition ─── +function cholesky(matrix) { + const n = matrix.length; + const L = Array.from({ length: n }, () => new Float64Array(n)); + for (let i = 0; i < n; i++) { + for (let j = 0; j <= i; j++) { + let sum = 0; + for (let k = 0; k < j; k++) { + sum += L[i][k] * L[j][k]; + } + if (i === j) { + const val = matrix[i][i] - sum; + L[i][j] = val > 0 ? Math.sqrt(val) : 0; + } else { + L[i][j] = L[j][j] > 0 ? (matrix[i][j] - sum) / L[j][j] : 0; + } + } + } + return L; +} + +// ─── Base Asset Class Definitions ─── +// These define the statistical properties for simulation +const BASE_CLASSES = { + us_equity: { mu: 0.10, sigma: 0.16, dfT: 8, crisisVol: 0.35, clusterMult: 1.2, clusterCap: 2.0 }, + us_tech: { mu: 0.12, sigma: 0.20, dfT: 7, crisisVol: 0.40, clusterMult: 1.2, clusterCap: 2.0 }, + us_small_cap: { mu: 0.11, sigma: 0.20, dfT: 7, crisisVol: 0.38, clusterMult: 1.2, clusterCap: 2.0 }, + intl_developed: { mu: 0.08, sigma: 0.17, dfT: 8, crisisVol: 0.30, clusterMult: 1.15, clusterCap: 1.8 }, + intl_emerging: { mu: 0.09, sigma: 0.22, dfT: 6, crisisVol: 0.38, clusterMult: 1.2, clusterCap: 2.0 }, + us_aggregate_bond: { mu: 0.04, sigma: 0.06, dfT: 20, crisisVol: 0.10, clusterMult: 1.05, clusterCap: 1.5 }, + us_long_treasury: { mu: 0.04, sigma: 0.15, dfT: 15, crisisVol: 0.22, clusterMult: 1.1, clusterCap: 1.8 }, + tips: { mu: 0.035, sigma: 0.06, dfT: 20, crisisVol: 0.09, clusterMult: 1.05, clusterCap: 1.5 }, + gold: { mu: 0.05, sigma: 0.15, dfT: 10, crisisVol: 0.22, clusterMult: 1.1, clusterCap: 1.6 }, + commodities: { mu: 0.03, sigma: 0.18, dfT: 7, crisisVol: 0.30, clusterMult: 1.15, clusterCap: 1.8 }, + real_estate: { mu: 0.08, sigma: 0.20, dfT: 8, crisisVol: 0.35, clusterMult: 1.2, clusterCap: 2.0 }, + crypto_major: { mu: 0.15, sigma: 0.60, dfT: 5, crisisVol: 0.85, clusterMult: 1.25, clusterCap: 2.5 }, + crypto_alt: { mu: 0.12, sigma: 0.75, dfT: 4, crisisVol: 1.0, clusterMult: 1.3, clusterCap: 2.5 }, + managed_futures: { mu: 0.05, sigma: 0.12, dfT: 15, crisisVol: 0.18, clusterMult: 1.05, clusterCap: 1.5 }, + cash: { mu: 0.04, sigma: 0.005, dfT: null, crisisVol: 0.005, clusterMult: 1.0, clusterCap: 1.0 }, +}; + +// ─── Normal-Regime Correlation Matrix ─── +// Order: us_equity, us_tech, us_small_cap, intl_developed, intl_emerging, +// us_aggregate_bond, us_long_treasury, tips, gold, commodities, +// real_estate, crypto_major, crypto_alt, managed_futures, cash +const CLASS_ORDER = [ + 'us_equity', 'us_tech', 'us_small_cap', 'intl_developed', 'intl_emerging', + 'us_aggregate_bond', 'us_long_treasury', 'tips', 'gold', 'commodities', + 'real_estate', 'crypto_major', 'crypto_alt', 'managed_futures', 'cash', +]; + +const NORMAL_CORR = [ + // us_eq tech small intl em agg tlt tips gold comm reit btc altc trend cash + [1.00, 0.90, 0.88, 0.80, 0.72, -0.10, -0.25, 0.00, 0.05, 0.15, 0.65, 0.35, 0.30, 0.00, 0.00], // us_equity + [0.90, 1.00, 0.82, 0.75, 0.68, -0.12, -0.28, -0.02, 0.02, 0.10, 0.58, 0.40, 0.35, 0.00, 0.00], // us_tech + [0.88, 0.82, 1.00, 0.75, 0.70, -0.08, -0.20, 0.02, 0.05, 0.18, 0.70, 0.32, 0.28, 0.00, 0.00], // us_small_cap + [0.80, 0.75, 0.75, 1.00, 0.78, -0.05, -0.18, 0.02, 0.10, 0.20, 0.60, 0.30, 0.25, 0.00, 0.00], // intl_developed + [0.72, 0.68, 0.70, 0.78, 1.00, -0.02, -0.12, 0.05, 0.12, 0.28, 0.55, 0.32, 0.28, 0.00, 0.00], // intl_emerging + [-0.10, -0.12, -0.08, -0.05, -0.02, 1.00, 0.85, 0.70, 0.15, 0.00, -0.05, -0.10, -0.08, 0.00, 0.00], // us_agg_bond + [-0.25, -0.28, -0.20, -0.18, -0.12, 0.85, 1.00, 0.55, 0.18, -0.05, -0.12, -0.12, -0.10, 0.00, 0.00], // us_long_treasury + [0.00, -0.02, 0.02, 0.02, 0.05, 0.70, 0.55, 1.00, 0.20, 0.15, 0.02, -0.05, -0.03, 0.00, 0.00], // tips + [0.05, 0.02, 0.05, 0.10, 0.12, 0.15, 0.18, 0.20, 1.00, 0.25, 0.05, 0.20, 0.15, 0.05, 0.00], // gold + [0.15, 0.10, 0.18, 0.20, 0.28, 0.00, -0.05, 0.15, 0.25, 1.00, 0.15, 0.15, 0.12, 0.05, 0.00], // commodities + [0.65, 0.58, 0.70, 0.60, 0.55, -0.05, -0.12, 0.02, 0.05, 0.15, 1.00, 0.25, 0.20, 0.00, 0.00], // real_estate + [0.35, 0.40, 0.32, 0.30, 0.32, -0.10, -0.12, -0.05, 0.20, 0.15, 0.25, 1.00, 0.85, 0.00, 0.00], // crypto_major + [0.30, 0.35, 0.28, 0.25, 0.28, -0.08, -0.10, -0.03, 0.15, 0.12, 0.20, 0.85, 1.00, 0.00, 0.00], // crypto_alt + [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.05, 0.05, 0.00, 0.00, 0.00, 1.00, 0.00], // managed_futures + [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 1.00], // cash +]; + +// ─── Crisis Regime Definitions ─── +const CRISIS_REGIMES = { + standard: { + label: 'Standard (Deflationary)', + description: 'Stocks crash, bonds rally, gold flat, trend positive', + corrAdjust: { + // During standard crisis: stocks drop, bonds become more negatively correlated (flight to safety) + equityBondCorr: -0.45, + equityGoldCorr: 0.10, + equityTrendCorr: -0.15, + bondReturnBoost: 0.12, // Bonds rally in deflationary crisis + goldReturnBoost: 0.02, + trendReturnBoost: 0.10, + cashRateOverride: 0.005, + }, + }, + inflation: { + label: 'Inflationary', + description: 'Stocks crash, bonds crash, gold rallies, trend positive', + corrAdjust: { + equityBondCorr: 0.60, // Stocks and bonds fall together + equityGoldCorr: -0.15, // Gold diverges from stocks + equityTrendCorr: -0.15, + bondReturnBoost: -0.15, // Bonds crash + goldReturnBoost: 0.12, // Gold rallies + trendReturnBoost: 0.10, + cashRateOverride: 0.05, + }, + }, + liquidity: { + label: 'Liquidity Crisis', + description: 'Everything correlated, everything drops except cash', + corrAdjust: { + equityBondCorr: 0.40, + equityGoldCorr: 0.35, + equityTrendCorr: 0.20, + bondReturnBoost: -0.08, + goldReturnBoost: -0.05, + trendReturnBoost: 0.02, + cashRateOverride: 0.00, + }, + }, +}; + +// ─── Build Sub-Correlation Matrix ─── +function buildCorrelationMatrix(classIndices, regime, regimeWeights) { + const n = classIndices.length; + const matrix = Array.from({ length: n }, () => new Float64Array(n)); + + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + if (i === j) { + matrix[i][j] = 1.0; + } else { + matrix[i][j] = NORMAL_CORR[classIndices[i]][classIndices[j]]; + } + } + } + + // Apply crisis correlation adjustments (blended across regimes) + if (regime === 'crisis' && regimeWeights) { + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + const classI = CLASS_ORDER[classIndices[i]]; + const classJ = CLASS_ORDER[classIndices[j]]; + let corrAdj = 0; + + for (const [regimeName, weight] of Object.entries(regimeWeights)) { + if (weight <= 0) continue; + const r = CRISIS_REGIMES[regimeName]; + if (!r) continue; + + const isEquityI = isEquityClass(classI); + const isEquityJ = isEquityClass(classJ); + const isBondI = isBondClass(classI); + const isBondJ = isBondClass(classJ); + const isGoldI = classI === 'gold'; + const isGoldJ = classJ === 'gold'; + const isTrendI = classI === 'managed_futures'; + const isTrendJ = classJ === 'managed_futures'; + + if ((isEquityI && isBondJ) || (isBondI && isEquityJ)) { + corrAdj += weight * (r.corrAdjust.equityBondCorr - matrix[i][j]); + } else if ((isEquityI && isGoldJ) || (isGoldI && isEquityJ)) { + corrAdj += weight * (r.corrAdjust.equityGoldCorr - matrix[i][j]); + } else if ((isEquityI && isTrendJ) || (isTrendI && isEquityJ)) { + corrAdj += weight * (r.corrAdjust.equityTrendCorr - matrix[i][j]); + } else if (isEquityI && isEquityJ) { + // Equity correlations increase in crisis + corrAdj += weight * Math.min(0.15, 0.95 - matrix[i][j]); + } + } + + matrix[i][j] += corrAdj; + matrix[j][i] += corrAdj; + // Clamp + matrix[i][j] = Math.max(-0.99, Math.min(0.99, matrix[i][j])); + matrix[j][i] = matrix[i][j]; + } + } + } + + return matrix; +} + +function isEquityClass(cls) { + return ['us_equity', 'us_tech', 'us_small_cap', 'intl_developed', 'intl_emerging', 'real_estate'].includes(cls); +} + +function isBondClass(cls) { + return ['us_aggregate_bond', 'us_long_treasury', 'tips'].includes(cls); +} + +// ─── Blended Regime Parameters ─── +function getBlendedCrisisParams(regimeWeights) { + const params = { bondBoost: 0, goldBoost: 0, trendBoost: 0, cashRate: 0 }; + let totalWeight = 0; + for (const [name, weight] of Object.entries(regimeWeights)) { + if (weight <= 0 || !CRISIS_REGIMES[name]) continue; + const r = CRISIS_REGIMES[name].corrAdjust; + params.bondBoost += weight * r.bondReturnBoost; + params.goldBoost += weight * r.goldReturnBoost; + params.trendBoost += weight * r.trendReturnBoost; + params.cashRate += weight * r.cashRateOverride; + totalWeight += weight; + } + if (totalWeight > 0) { + params.bondBoost /= totalWeight; + params.goldBoost /= totalWeight; + params.trendBoost /= totalWeight; + params.cashRate /= totalWeight; + } + return params; +} + +// ─── Main Simulation ─── + +/** + * Run Monte Carlo simulation + * @param {Object} config + * @param {Array} config.assets - [{id, ticker, weight, baseClass, leverage, underlying, stackedComponents, expenseRatio}] + * @param {number} config.nPaths - Number of simulation paths (default 1200) + * @param {number} config.nYears - Simulation horizon in years (default 20) + * @param {Object} config.regimeWeights - {standard: 0.7, inflation: 0.3, liquidity: 0.0} (must sum to 1) + * @param {number} config.seed - Random seed (default 42) + * @returns {Object} results + */ +export function runMonteCarlo(config) { + const { + assets, + nPaths = 1200, + nYears = 20, + regimeWeights = { standard: 1.0, inflation: 0.0, liquidity: 0.0 }, + seed = 42, + } = config; + + const rng = mulberry32(seed); + const nMonths = nYears * 12; + const dt = 1 / 12; + + // Determine unique base classes needed + const baseClassSet = new Set(); + for (const asset of assets) { + if (asset.baseClass) baseClassSet.add(asset.baseClass); + if (asset.underlying) baseClassSet.add(asset.underlying); + if (asset.stackedComponents) { + for (const c of asset.stackedComponents) baseClassSet.add(c); + } + } + baseClassSet.add('cash'); // Always need cash for borrowing costs + const baseClasses = Array.from(baseClassSet); + const classIndices = baseClasses.map((c) => CLASS_ORDER.indexOf(c)); + + // Pre-compute Cholesky matrices + const normalCorr = buildCorrelationMatrix(classIndices, 'normal', null); + const crisisCorr = buildCorrelationMatrix(classIndices, 'crisis', regimeWeights); + const L_normal = cholesky(normalCorr); + const L_crisis = cholesky(crisisCorr); + + // Get crisis params + const crisisParams = getBlendedCrisisParams(regimeWeights); + + const N = baseClasses.length; + + // Storage for all paths + const allPaths = new Array(nPaths); + const allStats = { + finalMult: new Float64Array(nPaths), + cagr: new Float64Array(nPaths), + vol: new Float64Array(nPaths), + maxDD: new Float64Array(nPaths), + sharpe: new Float64Array(nPaths), + sortino: new Float64Array(nPaths), + }; + + for (let p = 0; p < nPaths; p++) { + const path = new Float64Array(nMonths + 1); + path[0] = 1.0; + const portMonthlyRets = new Float64Array(nMonths); + + // Volatility state per base class + const currentVols = new Float64Array(N); + for (let i = 0; i < N; i++) { + const bc = BASE_CLASSES[baseClasses[i]]; + currentVols[i] = bc.sigma * Math.sqrt(dt); + } + + let crisisTimer = 0; + + for (let m = 0; m < nMonths; m++) { + if (path[m] <= 0) { + path[m] = 0; + for (let rm = m; rm <= nMonths; rm++) path[rm] = 0; + break; + } + + const inCrisis = crisisTimer > 0; + const choleskyL = inCrisis ? L_crisis : L_normal; + + // Generate independent random draws + const z = new Float64Array(N); + for (let i = 0; i < N; i++) { + const bc = BASE_CLASSES[baseClasses[i]]; + z[i] = studentT(rng, bc.dfT); + } + + // Apply correlation via Cholesky + const correlated = new Float64Array(N); + for (let i = 0; i < N; i++) { + let sum = 0; + for (let j = 0; j <= i; j++) { + sum += choleskyL[i][j] * z[j]; + } + correlated[i] = sum; + } + + // Compute base class returns + const baseReturns = {}; + const cashIdx = baseClasses.indexOf('cash'); + const cashRate = inCrisis ? crisisParams.cashRate : BASE_CLASSES.cash.mu; + const cashReturn = cashRate * dt; + + for (let i = 0; i < N; i++) { + const className = baseClasses[i]; + const bc = BASE_CLASSES[className]; + + if (className === 'cash') { + baseReturns[className] = cashReturn; + continue; + } + + let vol = currentVols[i]; + let mu = bc.mu; + + // In crisis, apply regime-specific adjustments + if (inCrisis) { + vol = Math.max(vol, bc.crisisVol * Math.sqrt(dt)); + if (isBondClass(className)) { + mu = bc.mu + crisisParams.bondBoost; + } else if (className === 'gold') { + mu = bc.mu + crisisParams.goldBoost; + } else if (className === 'managed_futures') { + mu = bc.mu + crisisParams.trendBoost; + } + } + + let ret = mu * dt + correlated[i] * vol; + ret = Math.max(ret, -0.90); + baseReturns[className] = ret; + + // Volatility clustering + if (ret < -0.08) { + currentVols[i] = Math.min( + currentVols[i] * bc.clusterMult, + bc.sigma * Math.sqrt(dt) * bc.clusterCap + ); + } else { + currentVols[i] = currentVols[i] * 0.9 + bc.sigma * Math.sqrt(dt) * 0.1; + } + } + + // Crisis detection: any equity class drops > 8% + let equityCrash = false; + for (const cls of baseClasses) { + if (isEquityClass(cls) && baseReturns[cls] < -0.08) { + equityCrash = true; + break; + } + } + if (equityCrash) { + crisisTimer = 12; + } else if (crisisTimer > 0) { + crisisTimer--; + } + + // Compute portfolio asset returns from base class returns + let portReturn = 0; + for (const asset of assets) { + let assetReturn; + + if (asset.leverage && asset.leverage > 1 && asset.underlying) { + // Leveraged product + const underlyingRet = baseReturns[asset.underlying] || 0; + const underlyingVol = currentVols[baseClasses.indexOf(asset.underlying)] || 0; + const betaSlippage = asset.leverage * underlyingVol * underlyingVol; + assetReturn = + asset.leverage * underlyingRet - + (asset.leverage - 1) * cashReturn - + (asset.expenseRatio || 0) * dt - + betaSlippage; + } else if (asset.stackedComponents && asset.stackedComponents.length >= 2) { + // Stacked product (e.g., RSST = equity + trend overlay) + assetReturn = -cashReturn - (asset.expenseRatio || 0) * dt; + for (const comp of asset.stackedComponents) { + assetReturn += baseReturns[comp] || 0; + } + } else if (asset.baseClass) { + // Simple base class asset + assetReturn = (baseReturns[asset.baseClass] || 0) - (asset.expenseRatio || 0) * dt; + } else { + assetReturn = 0; + } + + assetReturn = Math.max(assetReturn, -0.99); + portReturn += (asset.weight / 100) * assetReturn; + } + + path[m + 1] = Math.max(0, path[m] * (1 + portReturn)); + portMonthlyRets[m] = portReturn; + } + + allPaths[p] = path; + const finalMult = path[nMonths]; + allStats.finalMult[p] = finalMult; + allStats.cagr[p] = finalMult > 0 ? (Math.pow(finalMult, 1 / nYears) - 1) * 100 : -100; + allStats.maxDD[p] = computeMaxDrawdown(path); + + // Monthly return statistics + const rf = 0.04 / 12; + const excess = portMonthlyRets.map((r) => r - rf); + const meanExcess = mean(excess); + const stdExcess = stddev(excess); + const downside = excess.filter((e) => e < 0); + const stdDownside = stddev(downside); + + allStats.vol[p] = stddev(portMonthlyRets) * Math.sqrt(12) * 100; + allStats.sharpe[p] = stdExcess > 0 ? (meanExcess / stdExcess) * Math.sqrt(12) : 0; + allStats.sortino[p] = stdDownside > 0 ? (meanExcess / stdDownside) * Math.sqrt(12) : 0; + } + + // Compute percentile paths for fan chart + const percentilePaths = computePercentilePaths(allPaths, nMonths); + + // Compute summary statistics + const summary = { + cagr: computeDistStats(allStats.cagr), + vol: computeDistStats(allStats.vol), + maxDD: computeDistStats(allStats.maxDD), + sharpe: computeDistStats(allStats.sharpe), + sortino: computeDistStats(allStats.sortino), + finalMult: computeDistStats(allStats.finalMult), + }; + + return { + paths: allPaths, + percentilePaths, + summary, + nPaths, + nYears, + nMonths, + }; +} + +// ─── Helper Statistics ─── + +function mean(arr) { + if (arr.length === 0) return 0; + let sum = 0; + for (let i = 0; i < arr.length; i++) sum += arr[i]; + return sum / arr.length; +} + +function stddev(arr) { + if (arr.length < 2) return 0; + const m = mean(arr); + let sum = 0; + for (let i = 0; i < arr.length; i++) { + const d = arr[i] - m; + sum += d * d; + } + return Math.sqrt(sum / (arr.length - 1)); +} + +function percentile(sorted, p) { + const idx = (p / 100) * (sorted.length - 1); + const lo = Math.floor(idx); + const hi = Math.ceil(idx); + const frac = idx - lo; + if (lo === hi) return sorted[lo]; + return sorted[lo] * (1 - frac) + sorted[hi] * frac; +} + +function computeMaxDrawdown(path) { + let peak = path[0]; + let maxDD = 0; + for (let i = 1; i < path.length; i++) { + if (path[i] > peak) peak = path[i]; + const dd = peak > 0 ? (peak - path[i]) / peak : 0; + if (dd > maxDD) maxDD = dd; + } + return maxDD * 100; +} + +function computeDistStats(values) { + const sorted = Array.from(values).sort((a, b) => a - b); + return { + p5: percentile(sorted, 5), + p10: percentile(sorted, 10), + p25: percentile(sorted, 25), + median: percentile(sorted, 50), + p75: percentile(sorted, 75), + p90: percentile(sorted, 90), + p95: percentile(sorted, 95), + mean: mean(sorted), + }; +} + +function computePercentilePaths(allPaths, nMonths) { + const nPaths = allPaths.length; + const result = { + p5: new Float64Array(nMonths + 1), + p10: new Float64Array(nMonths + 1), + p25: new Float64Array(nMonths + 1), + p50: new Float64Array(nMonths + 1), + p75: new Float64Array(nMonths + 1), + p90: new Float64Array(nMonths + 1), + p95: new Float64Array(nMonths + 1), + }; + + const temp = new Float64Array(nPaths); + for (let m = 0; m <= nMonths; m++) { + for (let p = 0; p < nPaths; p++) { + temp[p] = allPaths[p][m]; + } + temp.sort(); + result.p5[m] = percentile(temp, 5); + result.p10[m] = percentile(temp, 10); + result.p25[m] = percentile(temp, 25); + result.p50[m] = percentile(temp, 50); + result.p75[m] = percentile(temp, 75); + result.p90[m] = percentile(temp, 90); + result.p95[m] = percentile(temp, 95); + } + + return result; +} + +// ─── Backtest with Actual Returns ─── + +/** + * Run backtest using actual historical returns + * @param {Object} config + * @param {Array} config.assets - [{id, ticker, weight}] + * @param {Object} config.returnData - {ticker: {dates: [], returns: []}} + * @param {boolean} config.fillMissing - Whether to simulate missing history + * @param {Object} config.regimeWeights - For simulating missing data + * @returns {Object} backtest results + */ +export function runBacktest(config) { + const { assets, returnData, fillMissing = false, regimeWeights } = config; + + // Find the common date range + let allDates = new Set(); + const assetDates = {}; + for (const asset of assets) { + const data = returnData[asset.ticker]; + if (!data) continue; + assetDates[asset.ticker] = new Set(data.dates); + for (const d of data.dates) allDates.add(d); + } + + const sortedDates = Array.from(allDates).sort(); + if (sortedDates.length === 0) { + return { error: 'No data available for selected assets' }; + } + + // Find earliest date where all assets have data (or use fill) + let startIdx = 0; + if (!fillMissing) { + // Find the latest start date across all assets + for (const asset of assets) { + const data = returnData[asset.ticker]; + if (!data || data.dates.length === 0) continue; + const assetStart = data.dates[0]; + const idx = sortedDates.indexOf(assetStart); + if (idx > startIdx) startIdx = idx; + } + } + + const dates = sortedDates.slice(startIdx); + const nMonths = dates.length; + + // Build portfolio path + const path = new Float64Array(nMonths + 1); + path[0] = 1.0; + const monthlyRets = new Float64Array(nMonths); + const dataAvailability = {}; + + for (const asset of assets) { + const data = returnData[asset.ticker]; + if (!data) { + dataAvailability[asset.ticker] = { available: false, startDate: null, endDate: null }; + continue; + } + dataAvailability[asset.ticker] = { + available: true, + startDate: data.dates[0], + endDate: data.dates[data.dates.length - 1], + totalMonths: data.dates.length, + }; + } + + for (let m = 0; m < nMonths; m++) { + const date = dates[m]; + let portReturn = 0; + let totalWeightUsed = 0; + + for (const asset of assets) { + const data = returnData[asset.ticker]; + if (!data) continue; + + const dateIdx = data.dates.indexOf(date); + let assetReturn; + + if (dateIdx >= 0) { + assetReturn = data.returns[dateIdx]; + } else if (fillMissing) { + // Simulate return based on available data from other assets + assetReturn = simulateMissingReturn(asset, date, assets, returnData, regimeWeights); + } else { + assetReturn = 0; + } + + portReturn += (asset.weight / 100) * assetReturn; + totalWeightUsed += asset.weight; + } + + // Scale if not all weight was used + if (totalWeightUsed > 0 && totalWeightUsed < 100) { + portReturn = portReturn * (100 / totalWeightUsed); + } + + path[m + 1] = Math.max(0, path[m] * (1 + portReturn)); + monthlyRets[m] = portReturn; + } + + const nYears = nMonths / 12; + const finalMult = path[nMonths]; + const rf = 0.04 / 12; + const excess = Array.from(monthlyRets).map((r) => r - rf); + const meanExcess = mean(excess); + const stdExcess = stddev(excess); + const downside = excess.filter((e) => e < 0); + const stdDownside = stddev(downside); + + return { + path: Array.from(path), + dates: ['Start', ...dates], + monthlyReturns: Array.from(monthlyRets), + dataAvailability, + stats: { + cagr: nYears > 0 && finalMult > 0 ? (Math.pow(finalMult, 1 / nYears) - 1) * 100 : -100, + vol: stddev(monthlyRets) * Math.sqrt(12) * 100, + maxDD: computeMaxDrawdown(path), + sharpe: stdExcess > 0 ? (meanExcess / stdExcess) * Math.sqrt(12) : 0, + sortino: stdDownside > 0 ? (meanExcess / stdDownside) * Math.sqrt(12) : 0, + finalMult, + nYears: nYears.toFixed(1), + startDate: dates[0], + endDate: dates[dates.length - 1], + }, + }; +} + +/** + * Simulate a missing return for an asset using conditional distribution + * Based on other assets' actual returns and known correlations + */ +function simulateMissingReturn(asset, date, allAssets, returnData, regimeWeights) { + // Collect actual returns from other assets at this date + const observedReturns = []; + const observedClasses = []; + + for (const other of allAssets) { + if (other.ticker === asset.ticker) continue; + const data = returnData[other.ticker]; + if (!data) continue; + const idx = data.dates.indexOf(date); + if (idx >= 0) { + observedReturns.push(data.returns[idx]); + observedClasses.push(other.baseClass || 'us_equity'); + } + } + + if (observedReturns.length === 0) { + // No other data available, use unconditional mean + const bc = BASE_CLASSES[asset.baseClass || 'us_equity']; + return bc.mu / 12; + } + + // Conditional mean: E[X|Y] = mu_X + Sigma_XY * Sigma_YY^{-1} * (Y - mu_Y) + const assetClass = asset.baseClass || 'us_equity'; + const bc = BASE_CLASSES[assetClass]; + const assetClassIdx = CLASS_ORDER.indexOf(assetClass); + + // Simple approximation: weighted average of correlations * observed deviations + let conditionalShift = 0; + for (let i = 0; i < observedReturns.length; i++) { + const otherClassIdx = CLASS_ORDER.indexOf(observedClasses[i]); + const corr = NORMAL_CORR[assetClassIdx][otherClassIdx]; + const otherBC = BASE_CLASSES[observedClasses[i]]; + const otherExpected = otherBC.mu / 12; + const otherVol = otherBC.sigma / Math.sqrt(12); + const deviation = (observedReturns[i] - otherExpected) / (otherVol || 1); + conditionalShift += corr * deviation; + } + conditionalShift /= observedReturns.length; + + const expectedReturn = bc.mu / 12; + const vol = bc.sigma / Math.sqrt(12); + + return expectedReturn + conditionalShift * vol; +} + +// ─── Exports ─── +export { BASE_CLASSES, CLASS_ORDER, CRISIS_REGIMES }; diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..5b467bd --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; +import './styles/index.css'; +import './styles/app.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/src/styles/app.css b/src/styles/app.css new file mode 100644 index 0000000..45d36ba --- /dev/null +++ b/src/styles/app.css @@ -0,0 +1,848 @@ +/* App Layout */ +.app { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.app-layout { + display: flex; + flex: 1; + overflow: hidden; +} + +.main-content { + flex: 1; + overflow: auto; + padding: var(--space-xl) var(--space-lg); + background: var(--bg-primary); + position: relative; +} + +.tab-content { + max-width: 1600px; + margin: 0 auto; +} + +/* ─── Header ─── */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--space-xl); + height: 52px; + background: var(--bg-primary); + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: 100; +} + +.header-left { + display: flex; + align-items: center; + gap: var(--space-xl); +} + +.logo { + display: flex; + align-items: center; + gap: 10px; +} + +.logo-icon { + width: 26px; + height: 26px; + background: var(--text-primary); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8125rem; + color: white; +} + +.logo-text { + font-size: 0.9375rem; + font-weight: 600; + color: var(--text-primary); + letter-spacing: -0.01em; +} + +.header-stats { + display: flex; + gap: 0; +} + +.header-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 var(--space-lg); + border-left: 1px solid var(--border-color-light); +} + +.header-stat:first-child { + border-left: none; +} + +.header-stat-value { + font-size: 0.9375rem; + font-weight: 600; + font-family: var(--font-mono); + line-height: 1.2; +} + +.header-stat-label { + font-size: 0.625rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1.2; +} + +.header-right { + display: flex; + align-items: center; + gap: var(--space-md); +} + +.sim-badge { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: 5px 12px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + font-size: 0.8125rem; + font-weight: 400; + color: var(--text-secondary); +} + +.sim-badge-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--accent-primary); +} + +.sim-badge.running .sim-badge-dot { + animation: pulse 1.2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.2; } +} + +/* ─── Sidebar ─── */ +.sidebar { + width: 220px; + background: var(--bg-primary); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + overflow-y: auto; + flex-shrink: 0; +} + +.sidebar-nav { + padding: var(--space-sm); + display: flex; + flex-direction: column; + gap: 2px; +} + +.sidebar-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-radius: var(--radius-lg); + font-size: 0.9375rem; + color: var(--text-primary); + cursor: pointer; + transition: background var(--transition-fast); + border: none; + background: transparent; + text-align: left; + width: 100%; + font-family: inherit; + font-weight: 400; +} + +.sidebar-item:hover { + background: rgba(0, 0, 0, 0.04); +} + +.sidebar-item.active { + font-weight: 700; +} + +.sidebar-item-icon { + font-size: 1.125rem; + width: 22px; + text-align: center; + flex-shrink: 0; + line-height: 1; +} + +.sidebar-item-label { + font-size: 0.9375rem; + line-height: 1.3; +} + +.sidebar-divider { + height: 1px; + background: var(--border-color-light); + margin: var(--space-sm); +} + +/* ─── Tab Headers ─── */ +.tab-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-lg); +} + +.tab-title { + font-size: 1.375rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; +} + +.tab-description { + color: var(--text-secondary); + margin-top: 3px; + font-size: 0.875rem; +} + +/* ─── Section Headers ─── */ +.section { + margin-bottom: var(--space-lg); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-sm); +} + +.section-title { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* ─── Portfolio Builder ─── */ +.search-input { + width: 100%; + padding: 10px 14px; + font-size: 0.875rem; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-primary); + transition: border-color var(--transition-fast); +} + +.search-input:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px rgba(29, 155, 240, 0.1); +} + +.asset-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 400px; + overflow-y: auto; + border: 1px solid var(--border-color-light); + border-radius: var(--radius-md); + padding: var(--space-xs); +} + +.asset-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-radius: var(--radius-md); + cursor: pointer; + transition: background var(--transition-fast); + border: none; + background: transparent; + width: 100%; + text-align: left; + font-family: inherit; +} + +.asset-item:hover { + background: var(--bg-tertiary); +} + +.asset-item.selected { + background: rgba(29, 155, 240, 0.06); +} + +.asset-item-left { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.asset-ticker { + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.8125rem; + min-width: 55px; +} + +.asset-name { + font-size: 0.8125rem; + color: var(--text-secondary); +} + +.asset-category-tag { + font-size: 0.625rem; + padding: 2px 6px; + border-radius: var(--radius-sm); + background: var(--bg-tertiary); + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.03em; + font-weight: 500; +} + +/* ─── Selected Assets / Weights ─── */ +.selected-assets { + display: flex; + flex-direction: column; + gap: 6px; +} + +.weight-row { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: 8px 12px; + background: var(--bg-primary); + border: 1px solid var(--border-color-light); + border-radius: var(--radius-md); +} + +.weight-row-ticker { + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.8125rem; + min-width: 55px; +} + +.weight-row-name { + font-size: 0.8125rem; + color: var(--text-secondary); + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.weight-slider { + width: 120px; + accent-color: var(--text-primary); +} + +.weight-input { + width: 58px; + text-align: right; + padding: 3px 6px; + font-size: 0.8125rem; + font-family: var(--font-mono); +} + +.weight-remove { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 2px; + font-size: 1rem; + line-height: 1; + transition: color var(--transition-fast); +} + +.weight-remove:hover { + color: var(--accent-danger); +} + +.weight-total { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + background: var(--bg-tertiary); + border-radius: var(--radius-md); + font-weight: 600; + margin-top: var(--space-sm); +} + +.weight-total-value { + font-family: var(--font-mono); + font-size: 1rem; +} + +.weight-total-value.over { + color: var(--accent-danger); +} + +.weight-total-value.under { + color: var(--accent-warning); +} + +.weight-total-value.exact { + color: var(--accent-success); +} + +/* ─── Preset Portfolios ─── */ +.preset-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: var(--space-sm); +} + +.preset-card { + padding: 10px 14px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); + background: var(--bg-primary); + text-align: left; + font-family: inherit; +} + +.preset-card:hover { + border-color: rgba(0, 0, 0, 0.3); + box-shadow: var(--shadow-sm); +} + +.preset-card-name { + font-weight: 600; + font-size: 0.8125rem; + margin-bottom: 2px; +} + +.preset-card-desc { + font-size: 0.6875rem; + color: var(--text-muted); +} + +/* ─── Simulation Config ─── */ +.config-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--space-lg); +} + +.config-block { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: var(--space-md) var(--space-lg); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-sm); +} + +.config-block-title { + font-weight: 600; + font-size: 0.9375rem; + margin-bottom: var(--space-md); + padding-bottom: var(--space-sm); + border-bottom: 1px solid var(--border-color-light); +} + +.config-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid var(--border-color-light); +} + +.config-row:last-child { + border-bottom: none; +} + +.config-label { + font-size: 0.8125rem; + color: var(--text-secondary); +} + +.config-value { + font-family: var(--font-mono); + font-size: 0.875rem; + font-weight: 500; + min-width: 60px; + text-align: right; +} + +.regime-slider-group { + display: flex; + flex-direction: column; + gap: var(--space-sm); + margin-top: var(--space-sm); +} + +.regime-slider-row { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.regime-slider-label { + font-size: 0.8125rem; + min-width: 120px; +} + +.regime-slider { + flex: 1; + accent-color: var(--text-primary); +} + +.regime-slider-value { + font-family: var(--font-mono); + font-size: 0.8125rem; + min-width: 40px; + text-align: right; +} + +.run-button { + width: 100%; + padding: 14px; + font-size: 1rem; + font-weight: 600; + background: var(--text-primary); + color: white; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-base); + font-family: inherit; + margin-top: var(--space-lg); +} + +.run-button:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + transform: translateY(-1px); +} + +.run-button:active { + transform: translateY(0); +} + +.run-button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.run-button.running { + background: var(--accent-primary); +} + +/* ─── Results Dashboard ─── */ +.results-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-lg); + flex-wrap: wrap; + gap: var(--space-md); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +.stat-card { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: var(--space-md) var(--space-lg); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-sm); + text-align: center; +} + +.stat-value { + font-size: 1.5rem; + font-weight: 700; + font-family: var(--font-mono); + letter-spacing: -0.02em; +} + +.stat-label { + font-size: 0.6875rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-top: 2px; +} + +.stat-range { + font-size: 0.6875rem; + color: var(--text-muted); + font-family: var(--font-mono); + margin-top: 4px; +} + +.chart-container { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: var(--space-lg); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-sm); + margin-bottom: var(--space-lg); +} + +.chart-title { + font-weight: 500; + font-size: 0.9375rem; + margin-bottom: var(--space-md); +} + +.chart-legend { + display: flex; + gap: var(--space-md); + flex-wrap: wrap; + margin-bottom: var(--space-sm); +} + +.legend-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.legend-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +/* ─── Percentile Table ─── */ +.percentile-table { + width: 100%; + border-collapse: collapse; +} + +.percentile-table th { + text-align: center; +} + +.percentile-table td { + text-align: center; +} + +/* ─── Actual Returns Panel ─── */ +.data-status { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.data-status-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border: 1px solid var(--border-color-light); + border-radius: var(--radius-md); +} + +.data-status-ticker { + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.8125rem; +} + +.data-status-info { + font-size: 0.75rem; + color: var(--text-muted); +} + +.data-status-badge { + font-size: 0.6875rem; + padding: 2px 8px; + border-radius: var(--radius-sm); +} + +.data-status-badge.available { + background: rgba(0, 186, 124, 0.08); + color: var(--status-positive); +} + +.data-status-badge.comparable { + background: rgba(245, 158, 11, 0.08); + color: var(--accent-warning); +} + +.data-status-badge.missing { + background: rgba(244, 33, 46, 0.08); + color: var(--status-negative); +} + +.fill-missing-toggle { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-md); + background: var(--bg-tertiary); + border-radius: var(--radius-md); + margin-top: var(--space-md); +} + +.fill-missing-toggle label { + margin-bottom: 0; + text-transform: none; + font-size: 0.8125rem; + color: var(--text-primary); + cursor: pointer; +} + +/* ─── Loading overlay ─── */ +.loading-overlay { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-2xl); + gap: var(--space-md); + color: var(--text-muted); +} + +.loading-overlay .spinner { + width: 32px; + height: 32px; + border-width: 2px; +} + +/* ─── Responsive ─── */ +@media (max-width: 1024px) { + .sidebar { + width: 56px; + } + + .sidebar-item .sidebar-item-label { + display: none; + } + + .sidebar-item { + justify-content: center; + padding: 10px; + } + + .sidebar-item-icon { + width: auto; + } + + .header-stats { + display: none; + } +} + +/* Mobile tab bar */ +.mobile-tab-bar { + display: none; +} + +@media (max-width: 768px) { + .sidebar { + display: none; + } + + .header { + padding: 0 var(--space-sm); + height: 44px; + } + + .logo-text { + font-size: 0.8125rem; + } + + .main-content { + padding: var(--space-sm) var(--space-sm) 64px; + } + + .config-grid { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .mobile-tab-bar { + display: flex; + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 56px; + background: var(--bg-primary); + border-top: 1px solid var(--border-color); + overflow-x: auto; + -webkit-overflow-scrolling: touch; + z-index: 100; + padding: 0 var(--space-xs); + gap: 0; + scrollbar-width: none; + } + + .mobile-tab-bar::-webkit-scrollbar { + display: none; + } + + .mobile-tab-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + min-width: 64px; + padding: 6px 8px; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + flex-shrink: 0; + font-family: inherit; + transition: color var(--transition-fast); + } + + .mobile-tab-item.active { + color: var(--text-primary); + font-weight: 600; + } + + .mobile-tab-icon { + font-size: 1.125rem; + line-height: 1; + } + + .mobile-tab-label { + font-size: 0.5625rem; + line-height: 1; + white-space: nowrap; + } +} diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 0000000..f26ddaf --- /dev/null +++ b/src/styles/index.css @@ -0,0 +1,510 @@ +/* Portfolio Monte Carlo Simulator - Global Styles */ +/* Clean, minimal: black text on white, thin borders, Inter + JetBrains Mono */ + +:root { + --bg-primary: #ffffff; + --bg-secondary: #ffffff; + --bg-tertiary: #f8f9fa; + --bg-elevated: #f0f1f3; + + --text-primary: #0f1419; + --text-secondary: #333639; + --text-muted: #65676b; + + --accent-primary: #1d9bf0; + --accent-hover: #1a8cd8; + --accent-secondary: #1e40af; + --accent-success: #00ba7c; + --accent-warning: #f59e0b; + --accent-danger: #f4212e; + + --status-positive: #00ba7c; + --status-negative: #f4212e; + --status-neutral: #1d9bf0; + + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; + --space-2xl: 3rem; + + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 10px; + --radius-xl: 14px; + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.03); + --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.05); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.06); + + --border-color: rgba(0, 0, 0, 0.22); + --border-color-light: rgba(0, 0, 0, 0.10); + + --transition-fast: 150ms ease; + --transition-base: 200ms ease; + --transition-slow: 350ms ease; + + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-sans); + background-color: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + min-height: 100vh; +} + +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.3; + color: var(--text-primary); + letter-spacing: -0.01em; +} + +h1 { font-size: 1.5rem; } +h2 { font-size: 1.25rem; } +h3 { font-size: 1.0625rem; font-weight: 500; } +h4 { font-size: 0.9375rem; font-weight: 500; } + +p { + color: var(--text-secondary); + margin-bottom: var(--space-md); + line-height: 1.55; +} + +a { + color: var(--accent-primary); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--accent-hover); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + padding: 8px 18px; + font-size: 0.875rem; + font-weight: 500; + border-radius: var(--radius-md); + border: none; + cursor: pointer; + transition: all var(--transition-base); + font-family: inherit; + line-height: 1.25; +} + +.btn-primary { + background: var(--accent-primary); + color: white; +} + +.btn-primary:hover { + background: var(--accent-hover); + box-shadow: 0 2px 12px rgba(29, 155, 240, 0.2); +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-secondary { + background: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background: var(--bg-tertiary); +} + +.btn-secondary.active { + border-color: var(--text-primary); + font-weight: 600; +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); +} + +.btn-ghost:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.btn-sm { + padding: 5px 12px; + font-size: 0.8125rem; +} + +.btn-danger { + background: transparent; + color: var(--accent-danger); + border: 1px solid rgba(244, 33, 46, 0.2); +} + +.btn-danger:hover { + background: rgba(244, 33, 46, 0.05); +} + +/* Form elements */ +input, select, textarea { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 6px 10px; + color: var(--text-primary); + font-family: inherit; + font-size: 0.875rem; + transition: border-color var(--transition-fast); + line-height: 1.4; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px rgba(29, 155, 240, 0.1); +} + +input[type="number"] { + font-family: var(--font-mono); + font-size: 0.8125rem; +} + +input[type="range"] { + border: none; + padding: 0; + background: transparent; + cursor: pointer; +} + +input[type="range"]:focus { + box-shadow: none; +} + +label { + display: block; + font-size: 0.75rem; + font-weight: 500; + color: var(--text-muted); + margin-bottom: var(--space-xs); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Cards */ +.card { + background: var(--bg-primary); + border-radius: var(--radius-lg); + padding: var(--space-lg); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-sm); +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-md); + padding-bottom: var(--space-sm); + border-bottom: 1px solid var(--border-color-light); +} + +.card-title { + font-size: 0.9375rem; + font-weight: 500; +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +th, td { + padding: 8px 12px; + text-align: left; + border-bottom: 1px solid var(--border-color-light); +} + +th { + font-weight: 500; + color: var(--text-muted); + text-transform: uppercase; + font-size: 0.6875rem; + letter-spacing: 0.05em; + background: var(--bg-tertiary); +} + +td { + font-family: var(--font-mono); + font-size: 0.8125rem; +} + +td.text-cell { + font-family: var(--font-sans); + font-size: 0.875rem; +} + +tr:hover { + background: rgba(0, 0, 0, 0.015); +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.25); +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + padding: 3px 9px; + font-size: 0.75rem; + font-weight: 500; + border-radius: var(--radius-sm); + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.badge-positive { + background: rgba(0, 186, 124, 0.08); + color: var(--status-positive); +} + +.badge-negative { + background: rgba(244, 33, 46, 0.08); + color: var(--status-negative); +} + +.badge-neutral { + background: rgba(29, 155, 240, 0.08); + color: var(--status-neutral); +} + +.badge-muted { + background: var(--bg-tertiary); + color: var(--text-muted); +} + +/* Metric display */ +.metric { + display: flex; + flex-direction: column; + gap: 3px; +} + +.metric-value { + font-size: 1.375rem; + font-weight: 600; + font-family: var(--font-mono); + color: var(--text-primary); + letter-spacing: -0.02em; +} + +.metric-label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 400; +} + +/* Grid */ +.grid { + display: grid; + gap: var(--space-md); +} + +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } + +@media (max-width: 1200px) { + .grid-4 { grid-template-columns: repeat(2, 1fr); } + .grid-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 768px) { + .grid-4, .grid-3, .grid-2 { grid-template-columns: 1fr; } +} + +/* Flex */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-sm { gap: var(--space-sm); } +.gap-md { gap: var(--space-md); } +.gap-lg { gap: var(--space-lg); } + +/* Animation */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(2px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 250ms ease; +} + +/* Spinner */ +.spinner { + width: 16px; + height: 16px; + border: 1.5px solid var(--border-color); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 0.9s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Tooltip-like info */ +.info-text { + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.4; +} + +.warning-text { + font-size: 0.75rem; + color: var(--accent-warning); + display: flex; + align-items: center; + gap: var(--space-xs); +} + +/* Selection */ +::selection { + background: rgba(29, 155, 240, 0.15); + color: var(--text-primary); +} + +/* Tabs */ +.tabs { + display: flex; + gap: 0; + border-bottom: 1px solid var(--border-color); +} + +.tab { + padding: 10px 16px; + font-size: 0.875rem; + font-weight: 400; + color: var(--text-muted); + background: transparent; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + transition: all var(--transition-fast); + margin-bottom: -1px; + font-family: inherit; +} + +.tab:hover { + color: var(--text-primary); +} + +.tab.active { + color: var(--text-primary); + border-bottom-color: var(--text-primary); + font-weight: 500; +} + +/* Toggle switch */ +.toggle-group { + display: inline-flex; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + overflow: hidden; +} + +.toggle-option { + padding: 6px 14px; + font-size: 0.8125rem; + font-weight: 400; + color: var(--text-secondary); + background: transparent; + border: none; + cursor: pointer; + transition: all var(--transition-fast); + font-family: inherit; + border-right: 1px solid var(--border-color-light); +} + +.toggle-option:last-child { + border-right: none; +} + +.toggle-option:hover { + background: var(--bg-tertiary); +} + +.toggle-option.active { + background: var(--text-primary); + color: white; + font-weight: 500; +} + +/* Empty state */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-2xl); + text-align: center; + color: var(--text-muted); +} + +.empty-state-icon { + font-size: 2.5rem; + margin-bottom: var(--space-md); + opacity: 0.3; +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..9ffe730 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: '/investing/', + build: { + outDir: 'dist', + }, +});