Spaces:
Sleeping
Sleeping
Scribbler310 commited on
Commit ·
dbc70ee
0
Parent(s):
feat: portfolio dashboard v1.0
Browse files- .dockerignore +6 -0
- .gitattributes +3 -0
- .gitignore +26 -0
- .hfignore +5 -0
- Dockerfile +32 -0
- README.md +21 -0
- Stock Portfolio - Sheet1.csv +6 -0
- Stocks data for tableau - Stock Prices.csv +1262 -0
- debug.js +48 -0
- eslint.config.js +21 -0
- index.html +13 -0
- nginx.conf +16 -0
- package-lock.json +0 -0
- package.json +40 -0
- postcss.config.js +6 -0
- public/favicon.svg +3 -0
- public/icons.svg +3 -0
- public/port-satellite.png +3 -0
- src/App.css +184 -0
- src/App.jsx +34 -0
- src/assets/hero.png +3 -0
- src/assets/react.svg +3 -0
- src/assets/vite.svg +3 -0
- src/components/FeeTransparencyModule.jsx +58 -0
- src/components/FinancialCalculators.jsx +503 -0
- src/components/Indicators.jsx +168 -0
- src/components/InvestmentCommittee.jsx +239 -0
- src/components/MacroTracker.jsx +217 -0
- src/components/PortfolioHeatmap.jsx +168 -0
- src/components/RebalancingEngine.jsx +42 -0
- src/components/StockPopup.jsx +174 -0
- src/components/StockSearch.jsx +318 -0
- src/components/TransparencyModal.jsx +189 -0
- src/data/historicalData.js +120 -0
- src/data/mockData.js +70 -0
- src/data/tickerMetrics.js +18 -0
- src/index.css +21 -0
- src/main.jsx +10 -0
- src/pages/Dashboard.jsx +410 -0
- src/pages/LandingPage.jsx +116 -0
- tailwind.config.js +21 -0
- vite.config.js +16 -0
.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.git
|
| 4 |
+
.env
|
| 5 |
+
.DS_Store
|
| 6 |
+
*.log
|
.gitattributes
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.svg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
| 25 |
+
|
| 26 |
+
.env
|
.hfignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
.git/
|
| 3 |
+
dist/
|
| 4 |
+
.env
|
| 5 |
+
package-lock.json
|
Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build the React application
|
| 2 |
+
FROM node:20-alpine as build-stage
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Copy package files and install dependencies
|
| 7 |
+
COPY package*.json ./
|
| 8 |
+
RUN npm install
|
| 9 |
+
|
| 10 |
+
# Copy the rest of the application code
|
| 11 |
+
COPY . .
|
| 12 |
+
|
| 13 |
+
# Build the app for production
|
| 14 |
+
# Note: VITE_ environment variables must be available at build time
|
| 15 |
+
RUN npm run build
|
| 16 |
+
|
| 17 |
+
# Stage 2: Serve the app with Nginx
|
| 18 |
+
FROM nginx:stable-alpine
|
| 19 |
+
|
| 20 |
+
# Copy the built assets from the build stage to Nginx's html directory
|
| 21 |
+
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
| 22 |
+
|
| 23 |
+
# Copy a custom Nginx configuration to handle React Router (SPA) routing
|
| 24 |
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
| 25 |
+
|
| 26 |
+
# Expose port 7860 (default for Hugging Face Spaces)
|
| 27 |
+
EXPOSE 7860
|
| 28 |
+
|
| 29 |
+
# Adjust Nginx to run on port 7860
|
| 30 |
+
RUN sed -i 's/listen\(.*\)80;/listen 7860;/' /etc/nginx/conf.d/default.conf
|
| 31 |
+
|
| 32 |
+
CMD ["nginx", "-g", "daemon off;"]
|
README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: GS Portfolio Dashboard
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Goldman Sachs Style Portfolio Dashboard
|
| 11 |
+
|
| 12 |
+
A premium, jargon-free investment portfolio management platform built with React, Tailwind CSS, and Framer Motion.
|
| 13 |
+
|
| 14 |
+
## Features
|
| 15 |
+
- **Radical Transparency**: Clear explanations of health scores and risk levels.
|
| 16 |
+
- **Dynamic Heatmap**: Visualize allocation and performance at a glance.
|
| 17 |
+
- **AI Investment Committee**: Real-time analysis of stock holdings.
|
| 18 |
+
- **Scenario Planning**: Macroeconomic rebalancing engine.
|
| 19 |
+
|
| 20 |
+
## Deployment
|
| 21 |
+
This app is dockerized and served via Nginx on port 7860 for Hugging Face Spaces.
|
Stock Portfolio - Sheet1.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Stock Name ,Unit Bought,Price When Purchased,Purchase Cost
|
| 2 |
+
OKTA,5,98.26,491.3
|
| 3 |
+
AMZN,5,181.96,909.8
|
| 4 |
+
NVDA,250,138,34500
|
| 5 |
+
SEDG,10,25.05,250.5
|
| 6 |
+
TSLA,5,244.5,1222.5
|
Stocks data for tableau - Stock Prices.csv
ADDED
|
@@ -0,0 +1,1262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Stock,Date,Close,Daily%change
|
| 2 |
+
OKTA ,5/2/2025 16:00:00,112.9,
|
| 3 |
+
OKTA ,5/5/2025 16:00:00,115.71,0.02488928255
|
| 4 |
+
OKTA ,5/6/2025 16:00:00,117.07,0.01175352174
|
| 5 |
+
OKTA ,5/7/2025 16:00:00,118.03,0.008200222089
|
| 6 |
+
OKTA ,5/8/2025 16:00:00,120.75,0.02304498856
|
| 7 |
+
OKTA ,5/9/2025 16:00:00,119.45,-0.01076604555
|
| 8 |
+
OKTA ,5/12/2025 16:00:00,124.17,0.03951444119
|
| 9 |
+
OKTA ,5/13/2025 16:00:00,124.05,-0.0009664170089
|
| 10 |
+
OKTA ,5/14/2025 16:00:00,123.34,-0.005723498589
|
| 11 |
+
OKTA ,5/15/2025 16:00:00,124.39,0.008513053348
|
| 12 |
+
OKTA ,5/16/2025 16:00:00,127.3,0.02339416352
|
| 13 |
+
OKTA ,5/19/2025 16:00:00,126.44,-0.006755695208
|
| 14 |
+
OKTA ,5/20/2025 16:00:00,125.54,-0.007118000633
|
| 15 |
+
OKTA ,5/21/2025 16:00:00,122.06,-0.02772024853
|
| 16 |
+
OKTA ,5/22/2025 16:00:00,122.06,0
|
| 17 |
+
OKTA ,5/23/2025 16:00:00,123.72,0.01359986892
|
| 18 |
+
OKTA ,5/27/2025 16:00:00,125.5,0.01438732622
|
| 19 |
+
OKTA ,5/28/2025 16:00:00,105.23,-0.1615139442
|
| 20 |
+
OKTA ,5/29/2025 16:00:00,106.63,0.01330419082
|
| 21 |
+
OKTA ,5/30/2025 16:00:00,103.17,-0.03244865422
|
| 22 |
+
OKTA ,6/2/2025 16:00:00,104.73,0.01512067461
|
| 23 |
+
OKTA ,6/3/2025 16:00:00,103.58,-0.01098061682
|
| 24 |
+
OKTA ,6/4/2025 16:00:00,105.6,0.01950183433
|
| 25 |
+
OKTA ,6/5/2025 16:00:00,104.18,-0.0134469697
|
| 26 |
+
OKTA ,6/6/2025 16:00:00,105.08,0.008638894222
|
| 27 |
+
OKTA ,6/9/2025 16:00:00,101.2,-0.03692424819
|
| 28 |
+
OKTA ,6/10/2025 16:00:00,100.78,-0.004150197628
|
| 29 |
+
OKTA ,6/11/2025 16:00:00,100.44,-0.003373685255
|
| 30 |
+
OKTA ,6/12/2025 16:00:00,100.18,-0.002588610115
|
| 31 |
+
OKTA ,6/13/2025 16:00:00,97.48,-0.02695148732
|
| 32 |
+
OKTA ,6/16/2025 16:00:00,99.28,0.01846532622
|
| 33 |
+
OKTA ,6/17/2025 16:00:00,98.67,-0.006144238517
|
| 34 |
+
OKTA ,6/18/2025 16:00:00,99,0.003344481605
|
| 35 |
+
OKTA ,6/20/2025 16:00:00,99.42,0.004242424242
|
| 36 |
+
OKTA ,6/23/2025 16:00:00,98.66,-0.007644337156
|
| 37 |
+
OKTA ,6/24/2025 16:00:00,98.53,-0.001317656598
|
| 38 |
+
OKTA ,6/25/2025 16:00:00,98.21,-0.003247741805
|
| 39 |
+
OKTA ,6/26/2025 16:00:00,98.13,-0.0008145809999
|
| 40 |
+
OKTA ,6/27/2025 16:00:00,98.43,0.003057169061
|
| 41 |
+
OKTA ,6/30/2025 16:00:00,99.97,0.01564563649
|
| 42 |
+
OKTA ,7/1/2025 16:00:00,98.55,-0.01420426128
|
| 43 |
+
OKTA ,7/2/2025 16:00:00,98.14,-0.004160324708
|
| 44 |
+
OKTA ,7/3/2025 13:05:00,99.11,0.009883839413
|
| 45 |
+
OKTA ,7/7/2025 16:00:00,97.4,-0.01725355665
|
| 46 |
+
OKTA ,7/8/2025 16:00:00,97.53,0.001334702259
|
| 47 |
+
OKTA ,7/9/2025 16:00:00,99.15,0.01661027376
|
| 48 |
+
OKTA ,7/10/2025 16:00:00,94.41,-0.04780635401
|
| 49 |
+
OKTA ,7/11/2025 16:00:00,91.56,-0.03018748014
|
| 50 |
+
OKTA ,7/14/2025 16:00:00,91.97,0.004477937964
|
| 51 |
+
OKTA ,7/15/2025 16:00:00,91.1,-0.009459606393
|
| 52 |
+
OKTA ,7/16/2025 16:00:00,91.07,-0.0003293084523
|
| 53 |
+
OKTA ,7/17/2025 16:00:00,92.1,0.01130998133
|
| 54 |
+
OKTA ,7/18/2025 16:00:00,95.43,0.03615635179
|
| 55 |
+
OKTA ,7/21/2025 16:00:00,95.86,0.00450592057
|
| 56 |
+
OKTA ,7/22/2025 16:00:00,95.65,-0.002190694763
|
| 57 |
+
OKTA ,7/23/2025 16:00:00,95.63,-0.0002090956613
|
| 58 |
+
OKTA ,7/24/2025 16:00:00,97.89,0.02363275123
|
| 59 |
+
OKTA ,7/25/2025 16:00:00,101.1,0.03279190929
|
| 60 |
+
OKTA ,7/28/2025 16:00:00,97.84,-0.03224530168
|
| 61 |
+
OKTA ,7/29/2025 16:00:00,98.99,0.01175388389
|
| 62 |
+
OKTA ,7/30/2025 16:00:00,99.77,0.007879583796
|
| 63 |
+
OKTA ,7/31/2025 16:00:00,97.8,-0.01974541445
|
| 64 |
+
OKTA ,8/1/2025 16:00:00,95.13,-0.0273006135
|
| 65 |
+
OKTA ,8/4/2025 16:00:00,97.72,0.0272259014
|
| 66 |
+
OKTA ,8/5/2025 16:00:00,95.98,-0.01780597626
|
| 67 |
+
OKTA ,8/6/2025 16:00:00,97.75,0.01844134195
|
| 68 |
+
OKTA ,8/7/2025 16:00:00,93.58,-0.04265984655
|
| 69 |
+
OKTA ,8/8/2025 16:00:00,91.55,-0.02169266937
|
| 70 |
+
OKTA ,8/11/2025 16:00:00,88.51,-0.03320589842
|
| 71 |
+
OKTA ,8/12/2025 16:00:00,89.33,0.009264489888
|
| 72 |
+
OKTA ,8/13/2025 16:00:00,90.98,0.01847083846
|
| 73 |
+
OKTA ,8/14/2025 16:00:00,88.61,-0.02604968125
|
| 74 |
+
OKTA ,8/15/2025 16:00:00,92.02,0.03848324117
|
| 75 |
+
OKTA ,8/18/2025 16:00:00,91.38,-0.00695500978
|
| 76 |
+
OKTA ,8/19/2025 16:00:00,91.15,-0.002516962136
|
| 77 |
+
OKTA ,8/20/2025 16:00:00,91.03,-0.001316511245
|
| 78 |
+
OKTA ,8/21/2025 16:00:00,89.78,-0.01373173679
|
| 79 |
+
OKTA ,8/22/2025 16:00:00,92.05,0.02528402762
|
| 80 |
+
OKTA ,8/25/2025 16:00:00,91.35,-0.007604562738
|
| 81 |
+
OKTA ,8/26/2025 16:00:00,91.56,0.002298850575
|
| 82 |
+
OKTA ,8/27/2025 16:00:00,93.03,0.01605504587
|
| 83 |
+
OKTA ,8/28/2025 16:00:00,92.59,-0.0047296571
|
| 84 |
+
OKTA ,8/29/2025 16:00:00,92.77,0.001944054434
|
| 85 |
+
OKTA ,9/2/2025 16:00:00,89.5,-0.03524846394
|
| 86 |
+
OKTA ,9/3/2025 16:00:00,89.82,0.003575418994
|
| 87 |
+
OKTA ,9/4/2025 16:00:00,89.74,-0.0008906702293
|
| 88 |
+
OKTA ,9/5/2025 16:00:00,91.48,0.019389347
|
| 89 |
+
OKTA ,9/8/2025 16:00:00,92.68,0.01311762134
|
| 90 |
+
OKTA ,9/9/2025 16:00:00,93.85,0.01262408287
|
| 91 |
+
OKTA ,9/10/2025 16:00:00,90.21,-0.03878529568
|
| 92 |
+
OKTA ,9/11/2025 16:00:00,91.96,0.01939917969
|
| 93 |
+
OKTA ,9/12/2025 16:00:00,90.34,-0.01761635494
|
| 94 |
+
OKTA ,9/15/2025 16:00:00,90.91,0.006309497454
|
| 95 |
+
OKTA ,9/16/2025 16:00:00,89.92,-0.0108898911
|
| 96 |
+
OKTA ,9/17/2025 16:00:00,90,0.0008896797153
|
| 97 |
+
OKTA ,9/18/2025 16:00:00,93.6,0.04
|
| 98 |
+
OKTA ,9/19/2025 16:00:00,93.37,-0.002457264957
|
| 99 |
+
OKTA ,9/22/2025 16:00:00,92.38,-0.0106029774
|
| 100 |
+
OKTA ,9/23/2025 16:00:00,92.2,-0.001948473696
|
| 101 |
+
OKTA ,9/24/2025 16:00:00,92.2,0
|
| 102 |
+
OKTA ,9/25/2025 16:00:00,91.19,-0.01095444685
|
| 103 |
+
OKTA ,9/26/2025 16:00:00,91.16,-0.0003289834412
|
| 104 |
+
OKTA ,9/29/2025 16:00:00,93.86,0.02961825362
|
| 105 |
+
OKTA ,9/30/2025 16:00:00,91.7,-0.02301299808
|
| 106 |
+
OKTA ,10/1/2025 16:00:00,91.69,-0.0001090512541
|
| 107 |
+
OKTA ,10/2/2025 16:00:00,94.92,0.03522739666
|
| 108 |
+
OKTA ,10/3/2025 16:00:00,93.3,-0.01706700379
|
| 109 |
+
OKTA ,10/6/2025 16:00:00,93.71,0.004394426581
|
| 110 |
+
OKTA ,10/7/2025 16:00:00,90.89,-0.03009283961
|
| 111 |
+
OKTA ,10/8/2025 16:00:00,92.63,0.01914402024
|
| 112 |
+
OKTA ,10/9/2025 16:00:00,93.64,0.01090359495
|
| 113 |
+
OKTA ,10/10/2025 16:00:00,89.35,-0.04581375481
|
| 114 |
+
OKTA ,10/13/2025 16:00:00,90.14,0.008841634024
|
| 115 |
+
OKTA ,10/14/2025 16:00:00,89.08,-0.01175948525
|
| 116 |
+
OKTA ,10/15/2025 16:00:00,88.35,-0.008194881006
|
| 117 |
+
OKTA ,10/16/2025 16:00:00,87.71,-0.007243916242
|
| 118 |
+
OKTA ,10/17/2025 16:00:00,87.43,-0.003192338388
|
| 119 |
+
OKTA ,10/20/2025 16:00:00,88.32,0.01017957223
|
| 120 |
+
OKTA ,10/21/2025 16:00:00,89.45,0.01279438406
|
| 121 |
+
OKTA ,10/22/2025 16:00:00,87.04,-0.02694242594
|
| 122 |
+
OKTA ,10/23/2025 16:00:00,88.55,0.01734834559
|
| 123 |
+
OKTA ,10/24/2025 16:00:00,89.07,0.005872388481
|
| 124 |
+
OKTA ,10/27/2025 16:00:00,90.01,0.01055349725
|
| 125 |
+
OKTA ,10/28/2025 16:00:00,89.31,-0.007776913676
|
| 126 |
+
OKTA ,10/29/2025 16:00:00,87.65,-0.01858694435
|
| 127 |
+
OKTA ,10/30/2025 16:00:00,87.91,0.002966343411
|
| 128 |
+
OKTA ,10/31/2025 16:00:00,91.53,0.04117847799
|
| 129 |
+
OKTA ,11/3/2025 16:00:00,91.08,-0.004916420846
|
| 130 |
+
OKTA ,11/4/2025 16:00:00,86.97,-0.04512516469
|
| 131 |
+
OKTA ,11/5/2025 16:00:00,87.13,0.001839714844
|
| 132 |
+
OKTA ,11/6/2025 16:00:00,85.87,-0.01446115001
|
| 133 |
+
OKTA ,11/7/2025 16:00:00,85.21,-0.007686037033
|
| 134 |
+
OKTA ,11/10/2025 16:00:00,85.74,0.006219927239
|
| 135 |
+
OKTA ,11/11/2025 16:00:00,85.58,-0.001866106835
|
| 136 |
+
OKTA ,11/12/2025 16:00:00,84.69,-0.01039962608
|
| 137 |
+
OKTA ,11/13/2025 16:00:00,83.76,-0.01098122565
|
| 138 |
+
OKTA ,11/14/2025 16:00:00,83.94,0.002148997135
|
| 139 |
+
OKTA ,11/17/2025 16:00:00,81.07,-0.03419108887
|
| 140 |
+
OKTA ,11/18/2025 16:00:00,81.03,-0.0004934007648
|
| 141 |
+
OKTA ,11/19/2025 16:00:00,80.09,-0.01160064174
|
| 142 |
+
OKTA ,11/20/2025 16:00:00,78.32,-0.02210013735
|
| 143 |
+
OKTA ,11/21/2025 16:00:00,78.68,0.004596527068
|
| 144 |
+
OKTA ,11/24/2025 16:00:00,79.15,0.005973563803
|
| 145 |
+
OKTA ,11/25/2025 16:00:00,81.16,0.02539481996
|
| 146 |
+
OKTA ,11/26/2025 16:00:00,80.56,-0.007392804337
|
| 147 |
+
OKTA ,11/28/2025 13:05:00,80.33,-0.002855014896
|
| 148 |
+
OKTA ,12/1/2025 16:00:00,80.64,0.00385908129
|
| 149 |
+
OKTA ,12/2/2025 16:00:00,81.87,0.01525297619
|
| 150 |
+
OKTA ,12/3/2025 16:00:00,86.34,0.05459875412
|
| 151 |
+
OKTA ,12/4/2025 16:00:00,85.9,-0.005096131573
|
| 152 |
+
OKTA ,12/5/2025 16:00:00,85.89,-0.0001164144354
|
| 153 |
+
OKTA ,12/8/2025 16:00:00,87.29,0.0162999185
|
| 154 |
+
OKTA ,12/9/2025 16:00:00,87.79,0.005728032993
|
| 155 |
+
OKTA ,12/10/2025 16:00:00,89.84,0.02335117895
|
| 156 |
+
OKTA ,12/11/2025 16:00:00,90.59,0.008348174533
|
| 157 |
+
OKTA ,12/12/2025 16:00:00,90.18,-0.004525885859
|
| 158 |
+
OKTA ,12/15/2025 16:00:00,88.2,-0.02195608782
|
| 159 |
+
OKTA ,12/16/2025 16:00:00,90.59,0.02709750567
|
| 160 |
+
OKTA ,12/17/2025 16:00:00,88.42,-0.02395407882
|
| 161 |
+
OKTA ,12/18/2025 16:00:00,90.23,0.02047048179
|
| 162 |
+
OKTA ,12/19/2025 16:00:00,90.21,-0.0002216557686
|
| 163 |
+
OKTA ,12/22/2025 16:00:00,90.94,0.008092229243
|
| 164 |
+
OKTA ,12/23/2025 16:00:00,89.06,-0.02067297119
|
| 165 |
+
OKTA ,12/24/2025 13:05:00,88.39,-0.00752301819
|
| 166 |
+
OKTA ,12/26/2025 16:00:00,88.61,0.00248896934
|
| 167 |
+
OKTA ,12/29/2025 16:00:00,88.08,-0.005981266223
|
| 168 |
+
OKTA ,12/30/2025 16:00:00,87.43,-0.007379654859
|
| 169 |
+
OKTA ,12/31/2025 16:00:00,86.47,-0.01098021274
|
| 170 |
+
OKTA ,1/2/2026 16:00:00,83.64,-0.0327281138
|
| 171 |
+
OKTA ,1/5/2026 16:00:00,87.71,0.04866092779
|
| 172 |
+
OKTA ,1/6/2026 16:00:00,90.36,0.0302132026
|
| 173 |
+
OKTA ,1/7/2026 16:00:00,93.84,0.0385126162
|
| 174 |
+
OKTA ,1/8/2026 16:00:00,93.93,0.0009590792839
|
| 175 |
+
OKTA ,1/9/2026 16:00:00,92.23,-0.01809858405
|
| 176 |
+
OKTA ,1/12/2026 16:00:00,93.57,0.01452889515
|
| 177 |
+
OKTA ,1/13/2026 16:00:00,94.07,0.005343593032
|
| 178 |
+
OKTA ,1/14/2026 16:00:00,93.35,-0.007653874774
|
| 179 |
+
OKTA ,1/15/2026 16:00:00,91.94,-0.01510444563
|
| 180 |
+
OKTA ,1/16/2026 16:00:00,89.55,-0.02599521427
|
| 181 |
+
OKTA ,1/20/2026 16:00:00,87.71,-0.02054718035
|
| 182 |
+
OKTA ,1/21/2026 16:00:00,88.94,0.01402348649
|
| 183 |
+
OKTA ,1/22/2026 16:00:00,91.46,0.02833370812
|
| 184 |
+
OKTA ,1/23/2026 16:00:00,90.76,-0.007653619068
|
| 185 |
+
OKTA ,1/26/2026 16:00:00,91.29,0.005839576906
|
| 186 |
+
OKTA ,1/27/2026 16:00:00,91.46,0.001862197393
|
| 187 |
+
OKTA ,1/28/2026 16:00:00,90.74,-0.007872293899
|
| 188 |
+
OKTA ,1/29/2026 16:00:00,85.69,-0.05565351554
|
| 189 |
+
OKTA ,1/30/2026 16:00:00,84.48,-0.01412066752
|
| 190 |
+
OKTA ,2/2/2026 16:00:00,88.13,0.04320549242
|
| 191 |
+
OKTA ,2/3/2026 16:00:00,82.31,-0.06603880631
|
| 192 |
+
OKTA ,2/4/2026 16:00:00,83.42,0.01348560321
|
| 193 |
+
OKTA ,2/5/2026 16:00:00,82.15,-0.01522416687
|
| 194 |
+
OKTA ,2/6/2026 16:00:00,86.74,0.05587340231
|
| 195 |
+
OKTA ,2/9/2026 16:00:00,88.18,0.01660133733
|
| 196 |
+
OKTA ,2/10/2026 16:00:00,88.45,0.003061918802
|
| 197 |
+
OKTA ,2/11/2026 16:00:00,88.18,-0.003052572075
|
| 198 |
+
OKTA ,2/12/2026 16:00:00,84.91,-0.03708323883
|
| 199 |
+
OKTA ,2/13/2026 16:00:00,87.26,0.02767636321
|
| 200 |
+
OKTA ,2/17/2026 16:00:00,82.46,-0.055008022
|
| 201 |
+
OKTA ,2/18/2026 16:00:00,82.93,0.005699733204
|
| 202 |
+
OKTA ,2/19/2026 16:00:00,81.8,-0.0136259496
|
| 203 |
+
OKTA ,2/20/2026 16:00:00,74.29,-0.09180929095
|
| 204 |
+
OKTA ,2/23/2026 16:00:00,69.51,-0.06434244178
|
| 205 |
+
OKTA ,2/24/2026 16:00:00,71.14,0.02344986333
|
| 206 |
+
OKTA ,2/25/2026 16:00:00,73.01,0.02628619623
|
| 207 |
+
OKTA ,2/26/2026 16:00:00,75.25,0.03068072867
|
| 208 |
+
OKTA ,2/27/2026 16:00:00,72.5,-0.0365448505
|
| 209 |
+
OKTA ,3/2/2026 16:00:00,73.97,0.02027586207
|
| 210 |
+
OKTA ,3/3/2026 16:00:00,72.52,-0.01960254157
|
| 211 |
+
OKTA ,3/4/2026 16:00:00,71.74,-0.01075565361
|
| 212 |
+
OKTA ,3/5/2026 16:00:00,79.65,0.1102592696
|
| 213 |
+
OKTA ,3/6/2026 16:00:00,80.72,0.01343377276
|
| 214 |
+
OKTA ,3/9/2026 16:00:00,79.71,-0.0125123885
|
| 215 |
+
OKTA ,3/10/2026 16:00:00,79.61,-0.001254547736
|
| 216 |
+
OKTA ,3/11/2026 16:00:00,80.85,0.01557593267
|
| 217 |
+
OKTA ,3/12/2026 16:00:00,78.95,-0.02350030921
|
| 218 |
+
OKTA ,3/13/2026 16:00:00,79.16,0.002659911336
|
| 219 |
+
OKTA ,3/16/2026 16:00:00,77.16,-0.0252652855
|
| 220 |
+
OKTA ,3/17/2026 16:00:00,78.53,0.01775531363
|
| 221 |
+
OKTA ,3/18/2026 16:00:00,78.43,-0.001273398701
|
| 222 |
+
OKTA ,3/19/2026 16:00:00,80.76,0.02970801989
|
| 223 |
+
OKTA ,3/20/2026 16:00:00,78.41,-0.02909856365
|
| 224 |
+
OKTA ,3/23/2026 16:00:00,81.1,0.03430684862
|
| 225 |
+
OKTA ,3/24/2026 16:00:00,76.76,-0.05351418002
|
| 226 |
+
OKTA ,3/25/2026 16:00:00,78.12,0.01771756123
|
| 227 |
+
OKTA ,3/26/2026 16:00:00,79.38,0.01612903226
|
| 228 |
+
OKTA ,3/27/2026 16:00:00,73.23,-0.07747543462
|
| 229 |
+
OKTA ,3/30/2026 16:00:00,75.47,0.0305885566
|
| 230 |
+
OKTA ,3/31/2026 16:00:00,78.71,0.04293096595
|
| 231 |
+
OKTA ,4/1/2026 16:00:00,79.15,0.005590141024
|
| 232 |
+
OKTA ,4/2/2026 16:00:00,80.19,0.01313960834
|
| 233 |
+
OKTA ,4/6/2026 16:00:00,80.56,0.004614041651
|
| 234 |
+
OKTA ,4/7/2026 16:00:00,79.34,-0.01514399206
|
| 235 |
+
OKTA ,4/8/2026 16:00:00,76.04,-0.04159314343
|
| 236 |
+
OKTA ,4/9/2026 16:00:00,67.76,-0.1088900579
|
| 237 |
+
OKTA ,4/10/2026 16:00:00,62.93,-0.07128099174
|
| 238 |
+
OKTA ,4/13/2026 16:00:00,65.46,0.0402034006
|
| 239 |
+
OKTA ,4/14/2026 16:00:00,64.09,-0.02092881149
|
| 240 |
+
OKTA ,4/15/2026 16:00:00,67.35,0.05086596973
|
| 241 |
+
OKTA ,4/16/2026 16:00:00,72.01,0.06919079436
|
| 242 |
+
OKTA ,4/17/2026 16:00:00,72.25,0.003332870435
|
| 243 |
+
OKTA ,4/20/2026 16:00:00,75.76,0.04858131488
|
| 244 |
+
OKTA ,4/21/2026 16:00:00,77.64,0.02481520591
|
| 245 |
+
OKTA ,4/22/2026 16:00:00,78.7,0.01365275631
|
| 246 |
+
OKTA ,4/23/2026 16:00:00,76.04,-0.03379923761
|
| 247 |
+
OKTA ,4/24/2026 16:00:00,75.98,-0.0007890583903
|
| 248 |
+
OKTA ,4/27/2026 16:00:00,76.14,0.00210581732
|
| 249 |
+
OKTA ,4/28/2026 16:00:00,76.2,0.0007880220646
|
| 250 |
+
OKTA ,4/29/2026 16:00:00,76.16,-0.0005249343832
|
| 251 |
+
OKTA ,4/30/2026 16:00:00,73.65,-0.03295693277
|
| 252 |
+
OKTA ,5/1/2026 16:00:00,75.78,-0.004989495798
|
| 253 |
+
,,,
|
| 254 |
+
stock ,Date,Close,
|
| 255 |
+
SEDG,5/2/2025 16:00:00,13.1,
|
| 256 |
+
SEDG,5/5/2025 16:00:00,12.92,-0.01374045802
|
| 257 |
+
SEDG,5/6/2025 16:00:00,14.37,0.1122291022
|
| 258 |
+
SEDG,5/7/2025 16:00:00,14.77,0.02783576896
|
| 259 |
+
SEDG,5/8/2025 16:00:00,18.29,0.2383209208
|
| 260 |
+
SEDG,5/9/2025 16:00:00,19.84,0.08474576271
|
| 261 |
+
SEDG,5/12/2025 16:00:00,19.16,-0.03427419355
|
| 262 |
+
SEDG,5/13/2025 16:00:00,18.15,-0.05271398747
|
| 263 |
+
SEDG,5/14/2025 16:00:00,17.93,-0.01212121212
|
| 264 |
+
SEDG,5/15/2025 16:00:00,20.84,0.1622978249
|
| 265 |
+
SEDG,5/16/2025 16:00:00,22.02,0.056621881
|
| 266 |
+
SEDG,5/19/2025 16:00:00,20.76,-0.05722070845
|
| 267 |
+
SEDG,5/20/2025 16:00:00,20.65,-0.005298651252
|
| 268 |
+
SEDG,5/21/2025 16:00:00,19.84,-0.0392251816
|
| 269 |
+
SEDG,5/22/2025 16:00:00,19.84,0
|
| 270 |
+
SEDG,5/23/2025 16:00:00,16.7,-0.158266129
|
| 271 |
+
SEDG,5/27/2025 16:00:00,17.22,0.03113772455
|
| 272 |
+
SEDG,5/28/2025 16:00:00,16.92,-0.01742160279
|
| 273 |
+
SEDG,5/29/2025 16:00:00,16.6,-0.01891252955
|
| 274 |
+
SEDG,5/30/2025 16:00:00,17.86,0.07590361446
|
| 275 |
+
SEDG,6/2/2025 16:00:00,17.1,-0.04255319149
|
| 276 |
+
SEDG,6/3/2025 16:00:00,18.11,0.05906432749
|
| 277 |
+
SEDG,6/4/2025 16:00:00,17.47,-0.03533959139
|
| 278 |
+
SEDG,6/5/2025 16:00:00,17.74,0.01545506583
|
| 279 |
+
SEDG,6/6/2025 16:00:00,18.19,0.02536640361
|
| 280 |
+
SEDG,6/9/2025 16:00:00,18.72,0.0291368884
|
| 281 |
+
SEDG,6/10/2025 16:00:00,20.93,0.1180555556
|
| 282 |
+
SEDG,6/11/2025 16:00:00,20.83,-0.004777830865
|
| 283 |
+
SEDG,6/12/2025 16:00:00,21.02,0.009121459434
|
| 284 |
+
SEDG,6/13/2025 16:00:00,23.3,0.1084681256
|
| 285 |
+
SEDG,6/16/2025 16:00:00,23.98,0.02918454936
|
| 286 |
+
SEDG,6/17/2025 16:00:00,15.96,-0.3344453711
|
| 287 |
+
SEDG,6/18/2025 16:00:00,16.98,0.06390977444
|
| 288 |
+
SEDG,6/20/2025 16:00:00,16.52,-0.02709069494
|
| 289 |
+
SEDG,6/23/2025 16:00:00,16.06,-0.02784503632
|
| 290 |
+
SEDG,6/24/2025 16:00:00,18.93,0.1787048568
|
| 291 |
+
SEDG,6/25/2025 16:00:00,19.09,0.008452192287
|
| 292 |
+
SEDG,6/26/2025 16:00:00,20.07,0.05133577789
|
| 293 |
+
SEDG,6/27/2025 16:00:00,19.8,-0.0134529148
|
| 294 |
+
SEDG,6/30/2025 16:00:00,20.4,0.0303030303
|
| 295 |
+
SEDG,7/1/2025 16:00:00,21.86,0.07156862745
|
| 296 |
+
SEDG,7/2/2025 16:00:00,23.6,0.07959743824
|
| 297 |
+
SEDG,7/3/2025 13:05:00,27.54,0.1669491525
|
| 298 |
+
SEDG,7/7/2025 16:00:00,26.43,-0.04030501089
|
| 299 |
+
SEDG,7/8/2025 16:00:00,26.15,-0.01059402194
|
| 300 |
+
SEDG,7/9/2025 16:00:00,27.09,0.03594646272
|
| 301 |
+
SEDG,7/10/2025 16:00:00,27.57,0.01771871539
|
| 302 |
+
SEDG,7/11/2025 16:00:00,25.62,-0.07072905332
|
| 303 |
+
SEDG,7/14/2025 16:00:00,26.72,0.04293520687
|
| 304 |
+
SEDG,7/15/2025 16:00:00,27.37,0.02432634731
|
| 305 |
+
SEDG,7/16/2025 16:00:00,24.96,-0.08805261235
|
| 306 |
+
SEDG,7/17/2025 16:00:00,25.26,0.01201923077
|
| 307 |
+
SEDG,7/18/2025 16:00:00,26.62,0.05384006334
|
| 308 |
+
SEDG,7/21/2025 16:00:00,28.83,0.0830202855
|
| 309 |
+
SEDG,7/22/2025 16:00:00,31.86,0.1050988554
|
| 310 |
+
SEDG,7/23/2025 16:00:00,29.07,-0.08757062147
|
| 311 |
+
SEDG,7/24/2025 16:00:00,28.47,-0.02063983488
|
| 312 |
+
SEDG,7/25/2025 16:00:00,27.23,-0.0435546189
|
| 313 |
+
SEDG,7/28/2025 16:00:00,27.06,-0.006243114212
|
| 314 |
+
SEDG,7/29/2025 16:00:00,24.95,-0.07797487066
|
| 315 |
+
SEDG,7/30/2025 16:00:00,25.81,0.03446893788
|
| 316 |
+
SEDG,7/31/2025 16:00:00,25.66,-0.005811700891
|
| 317 |
+
SEDG,8/1/2025 16:00:00,25.55,-0.004286827747
|
| 318 |
+
SEDG,8/4/2025 16:00:00,25.63,0.00313111546
|
| 319 |
+
SEDG,8/5/2025 16:00:00,26.12,0.01911822083
|
| 320 |
+
SEDG,8/6/2025 16:00:00,25.79,-0.01263399694
|
| 321 |
+
SEDG,8/7/2025 16:00:00,24.42,-0.05312136487
|
| 322 |
+
SEDG,8/8/2025 16:00:00,24.94,0.02129402129
|
| 323 |
+
SEDG,8/11/2025 16:00:00,24.9,-0.001603849238
|
| 324 |
+
SEDG,8/12/2025 16:00:00,25.1,0.008032128514
|
| 325 |
+
SEDG,8/13/2025 16:00:00,26.46,0.05418326693
|
| 326 |
+
SEDG,8/14/2025 16:00:00,25.67,-0.029856387
|
| 327 |
+
SEDG,8/15/2025 16:00:00,30.06,0.1710167511
|
| 328 |
+
SEDG,8/18/2025 16:00:00,31.16,0.03659347971
|
| 329 |
+
SEDG,8/19/2025 16:00:00,31.99,0.02663671374
|
| 330 |
+
SEDG,8/20/2025 16:00:00,32.06,0.002188183807
|
| 331 |
+
SEDG,8/21/2025 16:00:00,30.21,-0.05770430443
|
| 332 |
+
SEDG,8/22/2025 16:00:00,34.3,0.1353856339
|
| 333 |
+
SEDG,8/25/2025 16:00:00,31.99,-0.06734693878
|
| 334 |
+
SEDG,8/26/2025 16:00:00,32.25,0.008127539856
|
| 335 |
+
SEDG,8/27/2025 16:00:00,33.09,0.02604651163
|
| 336 |
+
SEDG,8/28/2025 16:00:00,33.25,0.004835297673
|
| 337 |
+
SEDG,8/29/2025 16:00:00,33.82,0.01714285714
|
| 338 |
+
SEDG,9/2/2025 16:00:00,31.53,-0.06771141336
|
| 339 |
+
SEDG,9/3/2025 16:00:00,33.21,0.05328258801
|
| 340 |
+
SEDG,9/4/2025 16:00:00,34.16,0.02860584161
|
| 341 |
+
SEDG,9/5/2025 16:00:00,34.42,0.007611241218
|
| 342 |
+
SEDG,9/8/2025 16:00:00,33.44,-0.02847181871
|
| 343 |
+
SEDG,9/9/2025 16:00:00,30.04,-0.1016746411
|
| 344 |
+
SEDG,9/10/2025 16:00:00,29.42,-0.0206391478
|
| 345 |
+
SEDG,9/11/2025 16:00:00,29.49,0.002379333787
|
| 346 |
+
SEDG,9/12/2025 16:00:00,28.97,-0.01763309596
|
| 347 |
+
SEDG,9/15/2025 16:00:00,30.59,0.05591991716
|
| 348 |
+
SEDG,9/16/2025 16:00:00,33.11,0.0823798627
|
| 349 |
+
SEDG,9/17/2025 16:00:00,34.11,0.03020235578
|
| 350 |
+
SEDG,9/18/2025 16:00:00,34.71,0.01759014952
|
| 351 |
+
SEDG,9/19/2025 16:00:00,35.45,0.02131950447
|
| 352 |
+
SEDG,9/22/2025 16:00:00,38.58,0.08829337094
|
| 353 |
+
SEDG,9/23/2025 16:00:00,35.93,-0.06868843961
|
| 354 |
+
SEDG,9/24/2025 16:00:00,37.47,0.04286111884
|
| 355 |
+
SEDG,9/25/2025 16:00:00,37.9,0.01147584734
|
| 356 |
+
SEDG,9/26/2025 16:00:00,39.45,0.04089709763
|
| 357 |
+
SEDG,9/29/2025 16:00:00,37.68,-0.04486692015
|
| 358 |
+
SEDG,9/30/2025 16:00:00,37,-0.01804670913
|
| 359 |
+
SEDG,10/1/2025 16:00:00,38.62,0.04378378378
|
| 360 |
+
SEDG,10/2/2025 16:00:00,37.96,-0.01708959089
|
| 361 |
+
SEDG,10/3/2025 16:00:00,36.24,-0.04531085353
|
| 362 |
+
SEDG,10/6/2025 16:00:00,37.09,0.02345474614
|
| 363 |
+
SEDG,10/7/2025 16:00:00,35.55,-0.04152062551
|
| 364 |
+
SEDG,10/8/2025 16:00:00,35.66,0.003094233474
|
| 365 |
+
SEDG,10/9/2025 16:00:00,38.61,0.08272574313
|
| 366 |
+
SEDG,10/10/2025 16:00:00,35.06,-0.09194509195
|
| 367 |
+
SEDG,10/13/2025 16:00:00,36.46,0.03993154592
|
| 368 |
+
SEDG,10/14/2025 16:00:00,37.73,0.03483269336
|
| 369 |
+
SEDG,10/15/2025 16:00:00,40.53,0.07421150278
|
| 370 |
+
SEDG,10/16/2025 16:00:00,40.11,-0.0103626943
|
| 371 |
+
SEDG,10/17/2025 16:00:00,37.02,-0.0770381451
|
| 372 |
+
SEDG,10/20/2025 16:00:00,40,0.08049702863
|
| 373 |
+
SEDG,10/21/2025 16:00:00,38.77,-0.03075
|
| 374 |
+
SEDG,10/22/2025 16:00:00,37.5,-0.03275728656
|
| 375 |
+
SEDG,10/23/2025 16:00:00,37.82,0.008533333333
|
| 376 |
+
SEDG,10/24/2025 16:00:00,39.7,0.0497091486
|
| 377 |
+
SEDG,10/27/2025 16:00:00,39.74,0.001007556675
|
| 378 |
+
SEDG,10/28/2025 16:00:00,37.84,-0.04781077001
|
| 379 |
+
SEDG,10/29/2025 16:00:00,36.3,-0.04069767442
|
| 380 |
+
SEDG,10/30/2025 16:00:00,34.34,-0.05399449036
|
| 381 |
+
SEDG,10/31/2025 16:00:00,35.09,0.02184041934
|
| 382 |
+
SEDG,11/3/2025 16:00:00,32.87,-0.06326588772
|
| 383 |
+
SEDG,11/4/2025 16:00:00,31.82,-0.0319440219
|
| 384 |
+
SEDG,11/5/2025 16:00:00,41.02,0.2891263356
|
| 385 |
+
SEDG,11/6/2025 16:00:00,38.88,-0.05216967333
|
| 386 |
+
SEDG,11/7/2025 16:00:00,40,0.02880658436
|
| 387 |
+
SEDG,11/10/2025 16:00:00,45.38,0.1345
|
| 388 |
+
SEDG,11/11/2025 16:00:00,44.7,-0.0149845747
|
| 389 |
+
SEDG,11/12/2025 16:00:00,42.51,-0.04899328859
|
| 390 |
+
SEDG,11/13/2025 16:00:00,36.42,-0.1432604093
|
| 391 |
+
SEDG,11/14/2025 16:00:00,36.18,-0.006589785832
|
| 392 |
+
SEDG,11/17/2025 16:00:00,34.27,-0.05279159757
|
| 393 |
+
SEDG,11/18/2025 16:00:00,34.8,0.01546542165
|
| 394 |
+
SEDG,11/19/2025 16:00:00,33.46,-0.03850574713
|
| 395 |
+
SEDG,11/20/2025 16:00:00,33.09,-0.01105797968
|
| 396 |
+
SEDG,11/21/2025 16:00:00,34.45,0.04110003022
|
| 397 |
+
SEDG,11/24/2025 16:00:00,34.45,0
|
| 398 |
+
SEDG,11/25/2025 16:00:00,34.99,0.01567489115
|
| 399 |
+
SEDG,11/26/2025 16:00:00,35.47,0.0137182052
|
| 400 |
+
SEDG,11/28/2025 13:05:00,36.53,0.02988440936
|
| 401 |
+
SEDG,12/1/2025 16:00:00,32.92,-0.0988228853
|
| 402 |
+
SEDG,12/2/2025 16:00:00,32.66,-0.007897934386
|
| 403 |
+
SEDG,12/3/2025 16:00:00,31.61,-0.03214941825
|
| 404 |
+
SEDG,12/4/2025 16:00:00,31.94,0.01043973426
|
| 405 |
+
SEDG,12/5/2025 16:00:00,29.52,-0.07576706324
|
| 406 |
+
SEDG,12/8/2025 16:00:00,30.43,0.03082655827
|
| 407 |
+
SEDG,12/9/2025 16:00:00,30.26,-0.005586592179
|
| 408 |
+
SEDG,12/10/2025 16:00:00,31.56,0.04296100463
|
| 409 |
+
SEDG,12/11/2025 16:00:00,32.02,0.01457541191
|
| 410 |
+
SEDG,12/12/2025 16:00:00,29.53,-0.07776389756
|
| 411 |
+
SEDG,12/15/2025 16:00:00,28.54,-0.03352522858
|
| 412 |
+
SEDG,12/16/2025 16:00:00,29.48,0.03293622985
|
| 413 |
+
SEDG,12/17/2025 16:00:00,28.92,-0.01899592944
|
| 414 |
+
SEDG,12/18/2025 16:00:00,28.47,-0.01556016598
|
| 415 |
+
SEDG,12/19/2025 16:00:00,29.06,0.02072356867
|
| 416 |
+
SEDG,12/22/2025 16:00:00,30.91,0.06366139023
|
| 417 |
+
SEDG,12/23/2025 16:00:00,30.48,-0.01391135555
|
| 418 |
+
SEDG,12/24/2025 13:05:00,30.74,0.008530183727
|
| 419 |
+
SEDG,12/26/2025 16:00:00,30.45,-0.009433962264
|
| 420 |
+
SEDG,12/29/2025 16:00:00,29.19,-0.04137931034
|
| 421 |
+
SEDG,12/30/2025 16:00:00,29.02,-0.005823912299
|
| 422 |
+
SEDG,12/31/2025 16:00:00,28.85,-0.005858028946
|
| 423 |
+
SEDG,1/2/2026 16:00:00,31.36,0.0870017331
|
| 424 |
+
SEDG,1/5/2026 16:00:00,31.26,-0.00318877551
|
| 425 |
+
SEDG,1/6/2026 16:00:00,30.78,-0.01535508637
|
| 426 |
+
SEDG,1/7/2026 16:00:00,30.52,-0.008447043535
|
| 427 |
+
SEDG,1/8/2026 16:00:00,30.26,-0.008519003932
|
| 428 |
+
SEDG,1/9/2026 16:00:00,32.89,0.08691341705
|
| 429 |
+
SEDG,1/12/2026 16:00:00,35.31,0.07357859532
|
| 430 |
+
SEDG,1/13/2026 16:00:00,34.31,-0.02832058907
|
| 431 |
+
SEDG,1/14/2026 16:00:00,34.78,0.01369863014
|
| 432 |
+
SEDG,1/15/2026 16:00:00,33.84,-0.02702702703
|
| 433 |
+
SEDG,1/16/2026 16:00:00,33.91,0.00206855792
|
| 434 |
+
SEDG,1/20/2026 16:00:00,32.49,-0.04187555293
|
| 435 |
+
SEDG,1/21/2026 16:00:00,33.34,0.02616189597
|
| 436 |
+
SEDG,1/22/2026 16:00:00,34.54,0.03599280144
|
| 437 |
+
SEDG,1/23/2026 16:00:00,34.6,0.001737116387
|
| 438 |
+
SEDG,1/26/2026 16:00:00,34.41,-0.00549132948
|
| 439 |
+
SEDG,1/27/2026 16:00:00,34.81,0.01162452775
|
| 440 |
+
SEDG,1/28/2026 16:00:00,35.8,0.02844010342
|
| 441 |
+
SEDG,1/29/2026 16:00:00,34.04,-0.04916201117
|
| 442 |
+
SEDG,1/30/2026 16:00:00,30.95,-0.09077555817
|
| 443 |
+
SEDG,2/2/2026 16:00:00,30.64,-0.01001615509
|
| 444 |
+
SEDG,2/3/2026 16:00:00,30.97,0.01077023499
|
| 445 |
+
SEDG,2/4/2026 16:00:00,35.04,0.1314175008
|
| 446 |
+
SEDG,2/5/2026 16:00:00,33.28,-0.0502283105
|
| 447 |
+
SEDG,2/6/2026 16:00:00,35.92,0.07932692308
|
| 448 |
+
SEDG,2/9/2026 16:00:00,36.8,0.02449888641
|
| 449 |
+
SEDG,2/10/2026 16:00:00,36.79,-0.0002717391304
|
| 450 |
+
SEDG,2/11/2026 16:00:00,36.33,-0.01250339766
|
| 451 |
+
SEDG,2/12/2026 16:00:00,34.41,-0.05284888522
|
| 452 |
+
SEDG,2/13/2026 16:00:00,35.53,0.03254867771
|
| 453 |
+
SEDG,2/17/2026 16:00:00,37.13,0.04503236701
|
| 454 |
+
SEDG,2/18/2026 16:00:00,35.1,-0.05467277134
|
| 455 |
+
SEDG,2/19/2026 16:00:00,34.96,-0.003988603989
|
| 456 |
+
SEDG,2/20/2026 16:00:00,37.9,0.08409610984
|
| 457 |
+
SEDG,2/23/2026 16:00:00,39.38,0.03905013193
|
| 458 |
+
SEDG,2/24/2026 16:00:00,43.07,0.093702387
|
| 459 |
+
SEDG,2/25/2026 16:00:00,42.45,-0.01439517065
|
| 460 |
+
SEDG,2/26/2026 16:00:00,40.4,-0.04829210836
|
| 461 |
+
SEDG,2/27/2026 16:00:00,35.4,-0.1237623762
|
| 462 |
+
SEDG,3/2/2026 16:00:00,40.6,0.1468926554
|
| 463 |
+
SEDG,3/3/2026 16:00:00,37.81,-0.06871921182
|
| 464 |
+
SEDG,3/4/2026 16:00:00,37.94,0.003438243851
|
| 465 |
+
SEDG,3/5/2026 16:00:00,35.24,-0.07116499736
|
| 466 |
+
SEDG,3/6/2026 16:00:00,33.41,-0.05192962543
|
| 467 |
+
SEDG,3/9/2026 16:00:00,34.59,0.03531876684
|
| 468 |
+
SEDG,3/10/2026 16:00:00,38.11,0.1017635155
|
| 469 |
+
SEDG,3/11/2026 16:00:00,36.09,-0.05300446077
|
| 470 |
+
SEDG,3/12/2026 16:00:00,35.19,-0.02493765586
|
| 471 |
+
SEDG,3/13/2026 16:00:00,37.44,0.06393861893
|
| 472 |
+
SEDG,3/16/2026 16:00:00,40.71,0.08733974359
|
| 473 |
+
SEDG,3/17/2026 16:00:00,42.88,0.05330385655
|
| 474 |
+
SEDG,3/18/2026 16:00:00,44.88,0.04664179104
|
| 475 |
+
SEDG,3/19/2026 16:00:00,45.66,0.01737967914
|
| 476 |
+
SEDG,3/20/2026 16:00:00,51.73,0.1329391152
|
| 477 |
+
SEDG,3/23/2026 16:00:00,46.73,-0.09665571235
|
| 478 |
+
SEDG,3/24/2026 16:00:00,47.67,0.02011555746
|
| 479 |
+
SEDG,3/25/2026 16:00:00,51.28,0.07572897
|
| 480 |
+
SEDG,3/26/2026 16:00:00,50.27,-0.01969578783
|
| 481 |
+
SEDG,3/27/2026 16:00:00,51.76,0.0296399443
|
| 482 |
+
SEDG,3/30/2026 16:00:00,47.37,-0.08481452859
|
| 483 |
+
SEDG,3/31/2026 16:00:00,51.05,0.07768629935
|
| 484 |
+
SEDG,4/1/2026 16:00:00,51.87,0.01606268364
|
| 485 |
+
SEDG,4/2/2026 16:00:00,48.75,-0.06015037594
|
| 486 |
+
SEDG,4/6/2026 16:00:00,45.09,-0.07507692308
|
| 487 |
+
SEDG,4/7/2026 16:00:00,43.85,-0.02750055445
|
| 488 |
+
SEDG,4/8/2026 16:00:00,43.52,-0.007525655644
|
| 489 |
+
SEDG,4/9/2026 16:00:00,41.84,-0.03860294118
|
| 490 |
+
SEDG,4/10/2026 16:00:00,41.76,-0.001912045889
|
| 491 |
+
SEDG,4/13/2026 16:00:00,43.21,0.03472222222
|
| 492 |
+
SEDG,4/14/2026 16:00:00,42.98,-0.005322841935
|
| 493 |
+
SEDG,4/15/2026 16:00:00,37.83,-0.1198231736
|
| 494 |
+
SEDG,4/16/2026 16:00:00,38.91,0.02854877082
|
| 495 |
+
SEDG,4/17/2026 16:00:00,38.3,-0.0156772038
|
| 496 |
+
SEDG,4/20/2026 16:00:00,39.82,0.03968668407
|
| 497 |
+
SEDG,4/21/2026 16:00:00,40.57,0.0188347564
|
| 498 |
+
SEDG,4/22/2026 16:00:00,42.6,0.05003697313
|
| 499 |
+
SEDG,4/23/2026 16:00:00,47.36,0.1117370892
|
| 500 |
+
SEDG,4/24/2026 16:00:00,45.83,-0.03230574324
|
| 501 |
+
SEDG,4/27/2026 16:00:00,47.38,0.0338206415
|
| 502 |
+
SEDG,4/28/2026 16:00:00,44.29,-0.0652173913
|
| 503 |
+
SEDG,4/29/2026 16:00:00,41.58,-0.061187627
|
| 504 |
+
SEDG,4/30/2026 16:00:00,42.86,0.03078403078
|
| 505 |
+
SDEG,5/1/2026 16:00:00,42.91,0.001166588894
|
| 506 |
+
,,,
|
| 507 |
+
Stock,Date,Close,
|
| 508 |
+
AMZN,5/2/2025 16:00:00,189.98,
|
| 509 |
+
AMZN,5/5/2025 16:00:00,186.35,-0.01910727445
|
| 510 |
+
AMZN,5/6/2025 16:00:00,185.01,-0.007190770056
|
| 511 |
+
AMZN,5/7/2025 16:00:00,188.71,0.01999891898
|
| 512 |
+
AMZN,5/8/2025 16:00:00,192.08,0.01785808913
|
| 513 |
+
AMZN,5/9/2025 16:00:00,193.06,0.005102040816
|
| 514 |
+
AMZN,5/12/2025 16:00:00,208.64,0.08070030042
|
| 515 |
+
AMZN,5/13/2025 16:00:00,211.37,0.01308473926
|
| 516 |
+
AMZN,5/14/2025 16:00:00,210.25,-0.005298765198
|
| 517 |
+
AMZN,5/15/2025 16:00:00,205.17,-0.02416171225
|
| 518 |
+
AMZN,5/16/2025 16:00:00,205.59,0.002047082907
|
| 519 |
+
AMZN,5/19/2025 16:00:00,206.16,0.00277250839
|
| 520 |
+
AMZN,5/20/2025 16:00:00,204.07,-0.01013775708
|
| 521 |
+
AMZN,5/21/2025 16:00:00,201.12,-0.01445582398
|
| 522 |
+
AMZN,5/22/2025 16:00:00,201.12,0
|
| 523 |
+
AMZN,5/23/2025 16:00:00,200.99,-0.0006463802705
|
| 524 |
+
AMZN,5/27/2025 16:00:00,206.02,0.0250261207
|
| 525 |
+
AMZN,5/28/2025 16:00:00,204.72,-0.006310066984
|
| 526 |
+
AMZN,5/29/2025 16:00:00,205.7,0.004787026182
|
| 527 |
+
AMZN,5/30/2025 16:00:00,205.01,-0.003354399611
|
| 528 |
+
AMZN,6/2/2025 16:00:00,206.65,0.007999609775
|
| 529 |
+
AMZN,6/3/2025 16:00:00,205.71,-0.004548753932
|
| 530 |
+
AMZN,6/4/2025 16:00:00,207.23,0.007389042827
|
| 531 |
+
AMZN,6/5/2025 16:00:00,207.91,0.003281378179
|
| 532 |
+
AMZN,6/6/2025 16:00:00,213.57,0.02722331778
|
| 533 |
+
AMZN,6/9/2025 16:00:00,216.98,0.01596666198
|
| 534 |
+
AMZN,6/10/2025 16:00:00,217.61,0.00290349341
|
| 535 |
+
AMZN,6/11/2025 16:00:00,213.2,-0.02026561279
|
| 536 |
+
AMZN,6/12/2025 16:00:00,213.24,0.0001876172608
|
| 537 |
+
AMZN,6/13/2025 16:00:00,212.1,-0.005346088914
|
| 538 |
+
AMZN,6/16/2025 16:00:00,216.1,0.01885902876
|
| 539 |
+
AMZN,6/17/2025 16:00:00,214.82,-0.005923183711
|
| 540 |
+
AMZN,6/18/2025 16:00:00,212.52,-0.01070663812
|
| 541 |
+
AMZN,6/20/2025 16:00:00,209.69,-0.01331639375
|
| 542 |
+
AMZN,6/23/2025 16:00:00,208.47,-0.005818112452
|
| 543 |
+
AMZN,6/24/2025 16:00:00,212.77,0.02062646904
|
| 544 |
+
AMZN,6/25/2025 16:00:00,211.99,-0.003665930347
|
| 545 |
+
AMZN,6/26/2025 16:00:00,217.12,0.02419925468
|
| 546 |
+
AMZN,6/27/2025 16:00:00,223.3,0.02846352248
|
| 547 |
+
AMZN,6/30/2025 16:00:00,219.39,-0.01751007613
|
| 548 |
+
AMZN,7/1/2025 16:00:00,220.46,0.004877159397
|
| 549 |
+
AMZN,7/2/2025 16:00:00,219.92,-0.002449423932
|
| 550 |
+
AMZN,7/3/2025 13:05:00,223.41,0.01586940706
|
| 551 |
+
AMZN,7/7/2025 16:00:00,223.47,0.0002685645226
|
| 552 |
+
AMZN,7/8/2025 16:00:00,219.36,-0.01839173043
|
| 553 |
+
AMZN,7/9/2025 16:00:00,222.54,0.01449671772
|
| 554 |
+
AMZN,7/10/2025 16:00:00,222.26,-0.001258200773
|
| 555 |
+
AMZN,7/11/2025 16:00:00,225.02,0.01241788896
|
| 556 |
+
AMZN,7/14/2025 16:00:00,225.69,0.00297751311
|
| 557 |
+
AMZN,7/15/2025 16:00:00,226.35,0.00292436528
|
| 558 |
+
AMZN,7/16/2025 16:00:00,223.19,-0.01396068036
|
| 559 |
+
AMZN,7/17/2025 16:00:00,223.88,0.003091536359
|
| 560 |
+
AMZN,7/18/2025 16:00:00,226.13,0.0100500268
|
| 561 |
+
AMZN,7/21/2025 16:00:00,229.3,0.01401848494
|
| 562 |
+
AMZN,7/22/2025 16:00:00,227.47,-0.007980811164
|
| 563 |
+
AMZN,7/23/2025 16:00:00,228.29,0.003604870972
|
| 564 |
+
AMZN,7/24/2025 16:00:00,232.23,0.01725874984
|
| 565 |
+
AMZN,7/25/2025 16:00:00,231.44,-0.00340179994
|
| 566 |
+
AMZN,7/28/2025 16:00:00,232.79,0.005833045282
|
| 567 |
+
AMZN,7/29/2025 16:00:00,231.01,-0.007646376563
|
| 568 |
+
AMZN,7/30/2025 16:00:00,230.19,-0.003549629886
|
| 569 |
+
AMZN,7/31/2025 16:00:00,234.11,0.01702941049
|
| 570 |
+
AMZN,8/1/2025 16:00:00,214.75,-0.08269616847
|
| 571 |
+
AMZN,8/4/2025 16:00:00,211.65,-0.01443538999
|
| 572 |
+
AMZN,8/5/2025 16:00:00,213.75,0.009922041106
|
| 573 |
+
AMZN,8/6/2025 16:00:00,222.31,0.04004678363
|
| 574 |
+
AMZN,8/7/2025 16:00:00,223.13,0.003688543026
|
| 575 |
+
AMZN,8/8/2025 16:00:00,222.69,-0.001971944606
|
| 576 |
+
AMZN,8/11/2025 16:00:00,221.3,-0.006241860883
|
| 577 |
+
AMZN,8/12/2025 16:00:00,221.47,0.0007681879801
|
| 578 |
+
AMZN,8/13/2025 16:00:00,224.56,0.01395222829
|
| 579 |
+
AMZN,8/14/2025 16:00:00,230.98,0.02858924118
|
| 580 |
+
AMZN,8/15/2025 16:00:00,231.03,0.0002164689584
|
| 581 |
+
AMZN,8/18/2025 16:00:00,231.49,0.001991083409
|
| 582 |
+
AMZN,8/19/2025 16:00:00,228.01,-0.01503304678
|
| 583 |
+
AMZN,8/20/2025 16:00:00,223.81,-0.01842024473
|
| 584 |
+
AMZN,8/21/2025 16:00:00,221.95,-0.008310620616
|
| 585 |
+
AMZN,8/22/2025 16:00:00,228.84,0.03104302771
|
| 586 |
+
AMZN,8/25/2025 16:00:00,227.94,-0.003932878867
|
| 587 |
+
AMZN,8/26/2025 16:00:00,228.71,0.003378081951
|
| 588 |
+
AMZN,8/27/2025 16:00:00,229.12,0.001792663198
|
| 589 |
+
AMZN,8/28/2025 16:00:00,231.6,0.01082402235
|
| 590 |
+
AMZN,8/29/2025 16:00:00,229,-0.01122625216
|
| 591 |
+
AMZN,9/2/2025 16:00:00,225.34,-0.01598253275
|
| 592 |
+
AMZN,9/3/2025 16:00:00,225.99,0.002884530043
|
| 593 |
+
AMZN,9/4/2025 16:00:00,235.68,0.04287800345
|
| 594 |
+
AMZN,9/5/2025 16:00:00,232.33,-0.01421418873
|
| 595 |
+
AMZN,9/8/2025 16:00:00,235.84,0.01510782077
|
| 596 |
+
AMZN,9/9/2025 16:00:00,238.24,0.01017639077
|
| 597 |
+
AMZN,9/10/2025 16:00:00,230.33,-0.0332018133
|
| 598 |
+
AMZN,9/11/2025 16:00:00,229.95,-0.001649806799
|
| 599 |
+
AMZN,9/12/2025 16:00:00,228.15,-0.00782778865
|
| 600 |
+
AMZN,9/15/2025 16:00:00,231.43,0.01437650668
|
| 601 |
+
AMZN,9/16/2025 16:00:00,234.05,0.01132091777
|
| 602 |
+
AMZN,9/17/2025 16:00:00,231.62,-0.01038239692
|
| 603 |
+
AMZN,9/18/2025 16:00:00,231.23,-0.001683792419
|
| 604 |
+
AMZN,9/19/2025 16:00:00,231.48,0.001081174588
|
| 605 |
+
AMZN,9/22/2025 16:00:00,227.63,-0.01663210645
|
| 606 |
+
AMZN,9/23/2025 16:00:00,220.71,-0.03040021087
|
| 607 |
+
AMZN,9/24/2025 16:00:00,220.21,-0.002265416157
|
| 608 |
+
AMZN,9/25/2025 16:00:00,218.15,-0.009354706871
|
| 609 |
+
AMZN,9/26/2025 16:00:00,219.78,0.007471922989
|
| 610 |
+
AMZN,9/29/2025 16:00:00,222.17,0.01087451087
|
| 611 |
+
AMZN,9/30/2025 16:00:00,219.57,-0.01170275015
|
| 612 |
+
AMZN,10/1/2025 16:00:00,220.63,0.004827617616
|
| 613 |
+
AMZN,10/2/2025 16:00:00,222.41,0.008067805829
|
| 614 |
+
AMZN,10/3/2025 16:00:00,219.51,-0.01303898206
|
| 615 |
+
AMZN,10/6/2025 16:00:00,220.9,0.006332285545
|
| 616 |
+
AMZN,10/7/2025 16:00:00,221.78,0.003983703033
|
| 617 |
+
AMZN,10/8/2025 16:00:00,225.22,0.01551086662
|
| 618 |
+
AMZN,10/9/2025 16:00:00,227.74,0.01118905959
|
| 619 |
+
AMZN,10/10/2025 16:00:00,216.37,-0.04992535347
|
| 620 |
+
AMZN,10/13/2025 16:00:00,220.07,0.01710033739
|
| 621 |
+
AMZN,10/14/2025 16:00:00,216.39,-0.01672195211
|
| 622 |
+
AMZN,10/15/2025 16:00:00,215.57,-0.003789454226
|
| 623 |
+
AMZN,10/16/2025 16:00:00,214.47,-0.005102750847
|
| 624 |
+
AMZN,10/17/2025 16:00:00,213.04,-0.006667599198
|
| 625 |
+
AMZN,10/20/2025 16:00:00,216.48,0.0161472024
|
| 626 |
+
AMZN,10/21/2025 16:00:00,222.03,0.02563747228
|
| 627 |
+
AMZN,10/22/2025 16:00:00,217.95,-0.01837589515
|
| 628 |
+
AMZN,10/23/2025 16:00:00,221.09,0.01440697408
|
| 629 |
+
AMZN,10/24/2025 16:00:00,224.21,0.01411190013
|
| 630 |
+
AMZN,10/27/2025 16:00:00,226.97,0.01230988805
|
| 631 |
+
AMZN,10/28/2025 16:00:00,229.25,0.01004538045
|
| 632 |
+
AMZN,10/29/2025 16:00:00,230.3,0.004580152672
|
| 633 |
+
AMZN,10/30/2025 16:00:00,222.86,-0.03230568823
|
| 634 |
+
AMZN,10/31/2025 16:00:00,244.22,0.09584492507
|
| 635 |
+
AMZN,11/3/2025 16:00:00,254,0.04004586029
|
| 636 |
+
AMZN,11/4/2025 16:00:00,249.32,-0.01842519685
|
| 637 |
+
AMZN,11/5/2025 16:00:00,250.2,0.003529600513
|
| 638 |
+
AMZN,11/6/2025 16:00:00,243.04,-0.02861710631
|
| 639 |
+
AMZN,11/7/2025 16:00:00,244.41,0.005636932192
|
| 640 |
+
AMZN,11/10/2025 16:00:00,248.4,0.01632502762
|
| 641 |
+
AMZN,11/11/2025 16:00:00,249.1,0.002818035427
|
| 642 |
+
AMZN,11/12/2025 16:00:00,244.2,-0.01967081493
|
| 643 |
+
AMZN,11/13/2025 16:00:00,237.58,-0.02710892711
|
| 644 |
+
AMZN,11/14/2025 16:00:00,234.69,-0.0121643236
|
| 645 |
+
AMZN,11/17/2025 16:00:00,232.87,-0.007754910733
|
| 646 |
+
AMZN,11/18/2025 16:00:00,222.55,-0.04431657148
|
| 647 |
+
AMZN,11/19/2025 16:00:00,222.69,0.0006290721186
|
| 648 |
+
AMZN,11/20/2025 16:00:00,217.14,-0.02492253806
|
| 649 |
+
AMZN,11/21/2025 16:00:00,220.69,0.01634889933
|
| 650 |
+
AMZN,11/24/2025 16:00:00,226.28,0.02532964792
|
| 651 |
+
AMZN,11/25/2025 16:00:00,229.67,0.01498143893
|
| 652 |
+
AMZN,11/26/2025 16:00:00,229.16,-0.00222057735
|
| 653 |
+
AMZN,11/28/2025 13:05:00,233.22,0.01771687904
|
| 654 |
+
AMZN,12/1/2025 16:00:00,233.88,0.002829945974
|
| 655 |
+
AMZN,12/2/2025 16:00:00,234.42,0.002308876347
|
| 656 |
+
AMZN,12/3/2025 16:00:00,232.38,-0.008702329153
|
| 657 |
+
AMZN,12/4/2025 16:00:00,229.11,-0.01407177898
|
| 658 |
+
AMZN,12/5/2025 16:00:00,229.53,0.001833180568
|
| 659 |
+
AMZN,12/8/2025 16:00:00,226.89,-0.01150176448
|
| 660 |
+
AMZN,12/9/2025 16:00:00,227.92,0.004539644762
|
| 661 |
+
AMZN,12/10/2025 16:00:00,231.78,0.01693576694
|
| 662 |
+
AMZN,12/11/2025 16:00:00,230.28,-0.006471654155
|
| 663 |
+
AMZN,12/12/2025 16:00:00,226.19,-0.01776098662
|
| 664 |
+
AMZN,12/15/2025 16:00:00,222.54,-0.01613687608
|
| 665 |
+
AMZN,12/16/2025 16:00:00,222.56,0.00008987148378
|
| 666 |
+
AMZN,12/17/2025 16:00:00,221.27,-0.005796189792
|
| 667 |
+
AMZN,12/18/2025 16:00:00,226.76,0.02481131649
|
| 668 |
+
AMZN,12/19/2025 16:00:00,227.35,0.002601869818
|
| 669 |
+
AMZN,12/22/2025 16:00:00,228.43,0.004750384869
|
| 670 |
+
AMZN,12/23/2025 16:00:00,232.14,0.0162412993
|
| 671 |
+
AMZN,12/24/2025 13:05:00,232.38,0.001033858878
|
| 672 |
+
AMZN,12/26/2025 16:00:00,232.52,0.0006024614855
|
| 673 |
+
AMZN,12/29/2025 16:00:00,232.07,-0.001935317392
|
| 674 |
+
AMZN,12/30/2025 16:00:00,232.53,0.001982160555
|
| 675 |
+
AMZN,12/31/2025 16:00:00,230.82,-0.007353889821
|
| 676 |
+
AMZN,1/2/2026 16:00:00,226.5,-0.01871588251
|
| 677 |
+
AMZN,1/5/2026 16:00:00,233.06,0.02896247241
|
| 678 |
+
AMZN,1/6/2026 16:00:00,240.93,0.03376812838
|
| 679 |
+
AMZN,1/7/2026 16:00:00,241.56,0.002614867389
|
| 680 |
+
AMZN,1/8/2026 16:00:00,246.29,0.01958105647
|
| 681 |
+
AMZN,1/9/2026 16:00:00,247.38,0.004425677047
|
| 682 |
+
AMZN,1/12/2026 16:00:00,246.47,-0.003678551217
|
| 683 |
+
AMZN,1/13/2026 16:00:00,242.6,-0.01570170812
|
| 684 |
+
AMZN,1/14/2026 16:00:00,236.65,-0.02452596867
|
| 685 |
+
AMZN,1/15/2026 16:00:00,238.18,0.006465244031
|
| 686 |
+
AMZN,1/16/2026 16:00:00,239.12,0.003946595012
|
| 687 |
+
AMZN,1/20/2026 16:00:00,231,-0.03395784543
|
| 688 |
+
AMZN,1/21/2026 16:00:00,231.31,0.001341991342
|
| 689 |
+
AMZN,1/22/2026 16:00:00,234.34,0.01309930396
|
| 690 |
+
AMZN,1/23/2026 16:00:00,239.16,0.02056840488
|
| 691 |
+
AMZN,1/26/2026 16:00:00,238.42,-0.003094162903
|
| 692 |
+
AMZN,1/27/2026 16:00:00,244.68,0.02625618656
|
| 693 |
+
AMZN,1/28/2026 16:00:00,243.01,-0.006825241131
|
| 694 |
+
AMZN,1/29/2026 16:00:00,241.73,-0.005267272952
|
| 695 |
+
AMZN,1/30/2026 16:00:00,239.3,-0.01005253796
|
| 696 |
+
AMZN,2/2/2026 16:00:00,242.96,0.01529460928
|
| 697 |
+
AMZN,2/3/2026 16:00:00,238.62,-0.01786302272
|
| 698 |
+
AMZN,2/4/2026 16:00:00,232.99,-0.02359399883
|
| 699 |
+
AMZN,2/5/2026 16:00:00,222.69,-0.04420790592
|
| 700 |
+
AMZN,2/6/2026 16:00:00,210.32,-0.05554807131
|
| 701 |
+
AMZN,2/9/2026 16:00:00,208.72,-0.007607455306
|
| 702 |
+
AMZN,2/10/2026 16:00:00,206.96,-0.008432349559
|
| 703 |
+
AMZN,2/11/2026 16:00:00,204.08,-0.01391573251
|
| 704 |
+
AMZN,2/12/2026 16:00:00,199.6,-0.02195217562
|
| 705 |
+
AMZN,2/13/2026 16:00:00,198.79,-0.004058116232
|
| 706 |
+
AMZN,2/17/2026 16:00:00,201.15,0.01187182454
|
| 707 |
+
AMZN,2/18/2026 16:00:00,204.79,0.0180959483
|
| 708 |
+
AMZN,2/19/2026 16:00:00,204.86,0.0003418135651
|
| 709 |
+
AMZN,2/20/2026 16:00:00,210.11,0.02562725764
|
| 710 |
+
AMZN,2/23/2026 16:00:00,205.27,-0.02303555281
|
| 711 |
+
AMZN,2/24/2026 16:00:00,208.56,0.01602767087
|
| 712 |
+
AMZN,2/25/2026 16:00:00,210.64,0.009973149214
|
| 713 |
+
AMZN,2/26/2026 16:00:00,207.92,-0.01291302697
|
| 714 |
+
AMZN,2/27/2026 16:00:00,210,0.01000384763
|
| 715 |
+
AMZN,3/2/2026 16:00:00,208.39,-0.007666666667
|
| 716 |
+
AMZN,3/3/2026 16:00:00,208.73,0.001631556217
|
| 717 |
+
AMZN,3/4/2026 16:00:00,216.82,0.03875820438
|
| 718 |
+
AMZN,3/5/2026 16:00:00,218.94,0.009777695785
|
| 719 |
+
AMZN,3/6/2026 16:00:00,213.21,-0.02617155385
|
| 720 |
+
AMZN,3/9/2026 16:00:00,213.49,0.001313259228
|
| 721 |
+
AMZN,3/10/2026 16:00:00,214.33,0.00393461052
|
| 722 |
+
AMZN,3/11/2026 16:00:00,212.65,-0.007838380068
|
| 723 |
+
AMZN,3/12/2026 16:00:00,209.53,-0.01467199624
|
| 724 |
+
AMZN,3/13/2026 16:00:00,207.67,-0.008877010452
|
| 725 |
+
AMZN,3/16/2026 16:00:00,211.74,0.01959840131
|
| 726 |
+
AMZN,3/17/2026 16:00:00,215.2,0.01634079532
|
| 727 |
+
AMZN,3/18/2026 16:00:00,209.87,-0.02476765799
|
| 728 |
+
AMZN,3/19/2026 16:00:00,208.76,-0.005288988421
|
| 729 |
+
AMZN,3/20/2026 16:00:00,205.37,-0.01623874305
|
| 730 |
+
AMZN,3/23/2026 16:00:00,210.14,0.02322637191
|
| 731 |
+
AMZN,3/24/2026 16:00:00,207.24,-0.01380032359
|
| 732 |
+
AMZN,3/25/2026 16:00:00,211.71,0.02156919514
|
| 733 |
+
AMZN,3/26/2026 16:00:00,207.54,-0.019696755
|
| 734 |
+
AMZN,3/27/2026 16:00:00,199.34,-0.03951045582
|
| 735 |
+
AMZN,3/30/2026 16:00:00,200.95,0.008076652955
|
| 736 |
+
AMZN,3/31/2026 16:00:00,208.27,0.03642697188
|
| 737 |
+
AMZN,4/1/2026 16:00:00,210.57,0.01104335718
|
| 738 |
+
AMZN,4/2/2026 16:00:00,209.77,-0.003799211664
|
| 739 |
+
AMZN,4/6/2026 16:00:00,212.79,0.01439672022
|
| 740 |
+
AMZN,4/7/2026 16:00:00,213.77,0.004605479581
|
| 741 |
+
AMZN,4/8/2026 16:00:00,221.25,0.03499087805
|
| 742 |
+
AMZN,4/9/2026 16:00:00,233.65,0.05604519774
|
| 743 |
+
AMZN,4/10/2026 16:00:00,238.38,0.02024395463
|
| 744 |
+
AMZN,4/13/2026 16:00:00,239.89,0.006334424029
|
| 745 |
+
AMZN,4/14/2026 16:00:00,249.02,0.03805911043
|
| 746 |
+
AMZN,4/15/2026 16:00:00,248.5,-0.002088185688
|
| 747 |
+
AMZN,4/16/2026 16:00:00,249.7,0.004828973843
|
| 748 |
+
AMZN,4/17/2026 16:00:00,250.56,0.00344413296
|
| 749 |
+
AMZN,4/20/2026 16:00:00,248.28,-0.009099616858
|
| 750 |
+
AMZN,4/21/2026 16:00:00,249.91,0.006565168358
|
| 751 |
+
AMZN,4/22/2026 16:00:00,255.36,0.02180785083
|
| 752 |
+
AMZN,4/23/2026 16:00:00,255.08,-0.001096491228
|
| 753 |
+
AMZN,4/24/2026 16:00:00,263.99,0.03493021797
|
| 754 |
+
AMZN,4/27/2026 16:00:00,261.12,-0.01087162393
|
| 755 |
+
AMZN,4/28/2026 16:00:00,259.7,-0.005438112745
|
| 756 |
+
AMZN,4/29/2026 16:00:00,263.04,0.01286099345
|
| 757 |
+
AMZN,4/30/2026 16:00:00,265.06,0.007679440389
|
| 758 |
+
AMZN,5/1/2026 16:00:00,268.26,0.01984489051
|
| 759 |
+
Stock,Date,Close,
|
| 760 |
+
TSLA,5/2/2025 16:00:00,287.21,
|
| 761 |
+
TSLA,5/5/2025 16:00:00,280.26,-0.02419832179
|
| 762 |
+
TSLA,5/6/2025 16:00:00,275.35,-0.01751944623
|
| 763 |
+
TSLA,5/7/2025 16:00:00,276.22,0.003159615035
|
| 764 |
+
TSLA,5/8/2025 16:00:00,284.82,0.03113460285
|
| 765 |
+
TSLA,5/9/2025 16:00:00,298.26,0.04718769749
|
| 766 |
+
TSLA,5/12/2025 16:00:00,318.38,0.06745792262
|
| 767 |
+
TSLA,5/13/2025 16:00:00,334.07,0.04928073371
|
| 768 |
+
TSLA,5/14/2025 16:00:00,347.68,0.04073996468
|
| 769 |
+
TSLA,5/15/2025 16:00:00,342.82,-0.01397837092
|
| 770 |
+
TSLA,5/16/2025 16:00:00,349.98,0.02088559594
|
| 771 |
+
TSLA,5/19/2025 16:00:00,342.09,-0.02254414538
|
| 772 |
+
TSLA,5/20/2025 16:00:00,343.82,0.005057148704
|
| 773 |
+
TSLA,5/21/2025 16:00:00,334.62,-0.02675818742
|
| 774 |
+
TSLA,5/22/2025 16:00:00,334.62,0
|
| 775 |
+
TSLA,5/23/2025 16:00:00,339.34,0.01410555257
|
| 776 |
+
TSLA,5/27/2025 16:00:00,362.89,0.06939942241
|
| 777 |
+
TSLA,5/28/2025 16:00:00,356.9,-0.01650637934
|
| 778 |
+
TSLA,5/29/2025 16:00:00,358.43,0.004286915102
|
| 779 |
+
TSLA,5/30/2025 16:00:00,346.46,-0.03339564211
|
| 780 |
+
TSLA,6/2/2025 16:00:00,342.69,-0.01088148704
|
| 781 |
+
TSLA,6/3/2025 16:00:00,344.27,0.004610580992
|
| 782 |
+
TSLA,6/4/2025 16:00:00,332.05,-0.03549539606
|
| 783 |
+
TSLA,6/5/2025 16:00:00,284.7,-0.1425990062
|
| 784 |
+
TSLA,6/6/2025 16:00:00,295.14,0.03667017914
|
| 785 |
+
TSLA,6/9/2025 16:00:00,308.58,0.04553771092
|
| 786 |
+
TSLA,6/10/2025 16:00:00,326.09,0.05674379415
|
| 787 |
+
TSLA,6/11/2025 16:00:00,326.43,0.001042656935
|
| 788 |
+
TSLA,6/12/2025 16:00:00,319.11,-0.02242440952
|
| 789 |
+
TSLA,6/13/2025 16:00:00,325.31,0.01942903701
|
| 790 |
+
TSLA,6/16/2025 16:00:00,329.13,0.01174264548
|
| 791 |
+
TSLA,6/17/2025 16:00:00,316.35,-0.03882964178
|
| 792 |
+
TSLA,6/18/2025 16:00:00,322.05,0.01801801802
|
| 793 |
+
TSLA,6/20/2025 16:00:00,322.16,0.0003415618693
|
| 794 |
+
TSLA,6/23/2025 16:00:00,348.68,0.08231934443
|
| 795 |
+
TSLA,6/24/2025 16:00:00,340.47,-0.02354594471
|
| 796 |
+
TSLA,6/25/2025 16:00:00,327.55,-0.0379475431
|
| 797 |
+
TSLA,6/26/2025 16:00:00,325.78,-0.005403755152
|
| 798 |
+
TSLA,6/27/2025 16:00:00,323.63,-0.006599545706
|
| 799 |
+
TSLA,6/30/2025 16:00:00,317.66,-0.01844699194
|
| 800 |
+
TSLA,7/1/2025 16:00:00,300.71,-0.05335893723
|
| 801 |
+
TSLA,7/2/2025 16:00:00,315.65,0.04968241828
|
| 802 |
+
TSLA,7/3/2025 13:05:00,315.35,-0.0009504197687
|
| 803 |
+
TSLA,7/7/2025 16:00:00,293.94,-0.0678928175
|
| 804 |
+
TSLA,7/8/2025 16:00:00,297.81,0.01316595224
|
| 805 |
+
TSLA,7/9/2025 16:00:00,295.88,-0.00648064202
|
| 806 |
+
TSLA,7/10/2025 16:00:00,309.87,0.04728268217
|
| 807 |
+
TSLA,7/11/2025 16:00:00,313.51,0.01174686159
|
| 808 |
+
TSLA,7/14/2025 16:00:00,316.9,0.01081305222
|
| 809 |
+
TSLA,7/15/2025 16:00:00,310.78,-0.01931208583
|
| 810 |
+
TSLA,7/16/2025 16:00:00,321.67,0.03504086492
|
| 811 |
+
TSLA,7/17/2025 16:00:00,319.41,-0.007025833929
|
| 812 |
+
TSLA,7/18/2025 16:00:00,329.65,0.03205910898
|
| 813 |
+
TSLA,7/21/2025 16:00:00,328.49,-0.003518883664
|
| 814 |
+
TSLA,7/22/2025 16:00:00,332.11,0.01102012238
|
| 815 |
+
TSLA,7/23/2025 16:00:00,332.56,0.00135497275
|
| 816 |
+
TSLA,7/24/2025 16:00:00,305.3,-0.0819701708
|
| 817 |
+
TSLA,7/25/2025 16:00:00,316.06,0.03524402227
|
| 818 |
+
TSLA,7/28/2025 16:00:00,325.59,0.03015250269
|
| 819 |
+
TSLA,7/29/2025 16:00:00,321.2,-0.01348321509
|
| 820 |
+
TSLA,7/30/2025 16:00:00,319.04,-0.006724782067
|
| 821 |
+
TSLA,7/31/2025 16:00:00,308.27,-0.03375752257
|
| 822 |
+
TSLA,8/1/2025 16:00:00,302.63,-0.01829564992
|
| 823 |
+
TSLA,8/4/2025 16:00:00,309.26,0.02190794039
|
| 824 |
+
TSLA,8/5/2025 16:00:00,308.72,-0.001746103602
|
| 825 |
+
TSLA,8/6/2025 16:00:00,319.91,0.0362464369
|
| 826 |
+
TSLA,8/7/2025 16:00:00,322.27,0.007377074802
|
| 827 |
+
TSLA,8/8/2025 16:00:00,329.65,0.02290005275
|
| 828 |
+
TSLA,8/11/2025 16:00:00,339.03,0.02845442136
|
| 829 |
+
TSLA,8/12/2025 16:00:00,340.84,0.005338760582
|
| 830 |
+
TSLA,8/13/2025 16:00:00,339.38,-0.004283534796
|
| 831 |
+
TSLA,8/14/2025 16:00:00,335.58,-0.01119688844
|
| 832 |
+
TSLA,8/15/2025 16:00:00,330.56,-0.01495917516
|
| 833 |
+
TSLA,8/18/2025 16:00:00,335.16,0.01391577928
|
| 834 |
+
TSLA,8/19/2025 16:00:00,329.31,-0.01745435016
|
| 835 |
+
TSLA,8/20/2025 16:00:00,323.9,-0.01642828945
|
| 836 |
+
TSLA,8/21/2025 16:00:00,320.11,-0.01170114233
|
| 837 |
+
TSLA,8/22/2025 16:00:00,340.01,0.06216613039
|
| 838 |
+
TSLA,8/25/2025 16:00:00,346.6,0.01938178289
|
| 839 |
+
TSLA,8/26/2025 16:00:00,351.67,0.01462781304
|
| 840 |
+
TSLA,8/27/2025 16:00:00,349.6,-0.005886200131
|
| 841 |
+
TSLA,8/28/2025 16:00:00,345.98,-0.01035469108
|
| 842 |
+
TSLA,8/29/2025 16:00:00,333.87,-0.03500202324
|
| 843 |
+
TSLA,9/2/2025 16:00:00,329.36,-0.01350825171
|
| 844 |
+
TSLA,9/3/2025 16:00:00,334.09,0.01436118533
|
| 845 |
+
TSLA,9/4/2025 16:00:00,338.53,0.01328983208
|
| 846 |
+
TSLA,9/5/2025 16:00:00,350.84,0.03636309928
|
| 847 |
+
TSLA,9/8/2025 16:00:00,346.4,-0.01265534147
|
| 848 |
+
TSLA,9/9/2025 16:00:00,346.97,0.001645496536
|
| 849 |
+
TSLA,9/10/2025 16:00:00,347.79,0.002363316713
|
| 850 |
+
TSLA,9/11/2025 16:00:00,368.81,0.06043877052
|
| 851 |
+
TSLA,9/12/2025 16:00:00,395.94,0.07356091212
|
| 852 |
+
TSLA,9/15/2025 16:00:00,410.04,0.03561145628
|
| 853 |
+
TSLA,9/16/2025 16:00:00,421.62,0.02824114721
|
| 854 |
+
TSLA,9/17/2025 16:00:00,425.86,0.01005644894
|
| 855 |
+
TSLA,9/18/2025 16:00:00,416.85,-0.02115718781
|
| 856 |
+
TSLA,9/19/2025 16:00:00,426.07,0.02211826796
|
| 857 |
+
TSLA,9/22/2025 16:00:00,434.21,0.01910484193
|
| 858 |
+
TSLA,9/23/2025 16:00:00,425.85,-0.01925335667
|
| 859 |
+
TSLA,9/24/2025 16:00:00,442.79,0.039779265
|
| 860 |
+
TSLA,9/25/2025 16:00:00,423.39,-0.04381309424
|
| 861 |
+
TSLA,9/26/2025 16:00:00,440.4,0.04017572451
|
| 862 |
+
TSLA,9/29/2025 16:00:00,443.21,0.006380563124
|
| 863 |
+
TSLA,9/30/2025 16:00:00,444.72,0.003406962839
|
| 864 |
+
TSLA,10/1/2025 16:00:00,459.46,0.03314445044
|
| 865 |
+
TSLA,10/2/2025 16:00:00,436,-0.05105993993
|
| 866 |
+
TSLA,10/3/2025 16:00:00,429.83,-0.01415137615
|
| 867 |
+
TSLA,10/6/2025 16:00:00,453.25,0.05448665752
|
| 868 |
+
TSLA,10/7/2025 16:00:00,433.09,-0.04447876448
|
| 869 |
+
TSLA,10/8/2025 16:00:00,438.69,0.01293033781
|
| 870 |
+
TSLA,10/9/2025 16:00:00,435.54,-0.007180469124
|
| 871 |
+
TSLA,10/10/2025 16:00:00,413.49,-0.0506268081
|
| 872 |
+
TSLA,10/13/2025 16:00:00,435.9,0.05419719945
|
| 873 |
+
TSLA,10/14/2025 16:00:00,429.24,-0.01527873365
|
| 874 |
+
TSLA,10/15/2025 16:00:00,435.15,0.01376852111
|
| 875 |
+
TSLA,10/16/2025 16:00:00,428.75,-0.0147075721
|
| 876 |
+
TSLA,10/17/2025 16:00:00,439.31,0.02462973761
|
| 877 |
+
TSLA,10/20/2025 16:00:00,447.43,0.01848353099
|
| 878 |
+
TSLA,10/21/2025 16:00:00,442.6,-0.01079498469
|
| 879 |
+
TSLA,10/22/2025 16:00:00,438.97,-0.008201536376
|
| 880 |
+
TSLA,10/23/2025 16:00:00,448.98,0.02280338064
|
| 881 |
+
TSLA,10/24/2025 16:00:00,433.72,-0.03398815092
|
| 882 |
+
TSLA,10/27/2025 16:00:00,452.42,0.04311537397
|
| 883 |
+
TSLA,10/28/2025 16:00:00,460.55,0.01797002785
|
| 884 |
+
TSLA,10/29/2025 16:00:00,461.51,0.002084464228
|
| 885 |
+
TSLA,10/30/2025 16:00:00,440.1,-0.04639119412
|
| 886 |
+
TSLA,10/31/2025 16:00:00,456.56,0.03740059077
|
| 887 |
+
TSLA,11/3/2025 16:00:00,468.37,0.02586735588
|
| 888 |
+
TSLA,11/4/2025 16:00:00,444.26,-0.05147639687
|
| 889 |
+
TSLA,11/5/2025 16:00:00,462.07,0.04008913699
|
| 890 |
+
TSLA,11/6/2025 16:00:00,445.91,-0.03497305603
|
| 891 |
+
TSLA,11/7/2025 16:00:00,429.52,-0.03675629611
|
| 892 |
+
TSLA,11/10/2025 16:00:00,445.23,0.03657571242
|
| 893 |
+
TSLA,11/11/2025 16:00:00,439.62,-0.0126002291
|
| 894 |
+
TSLA,11/12/2025 16:00:00,430.6,-0.02051771985
|
| 895 |
+
TSLA,11/13/2025 16:00:00,401.99,-0.06644217371
|
| 896 |
+
TSLA,11/14/2025 16:00:00,404.35,0.005870792806
|
| 897 |
+
TSLA,11/17/2025 16:00:00,408.92,0.01130208977
|
| 898 |
+
TSLA,11/18/2025 16:00:00,401.25,-0.01875672503
|
| 899 |
+
TSLA,11/19/2025 16:00:00,403.99,0.006828660436
|
| 900 |
+
TSLA,11/20/2025 16:00:00,395.23,-0.02168370504
|
| 901 |
+
TSLA,11/21/2025 16:00:00,391.09,-0.01047491334
|
| 902 |
+
TSLA,11/24/2025 16:00:00,417.78,0.06824516096
|
| 903 |
+
TSLA,11/25/2025 16:00:00,419.4,0.003877638949
|
| 904 |
+
TSLA,11/26/2025 16:00:00,426.58,0.0171196948
|
| 905 |
+
TSLA,11/28/2025 13:05:00,430.17,0.008415771954
|
| 906 |
+
TSLA,12/1/2025 16:00:00,430.14,-0.00006973987028
|
| 907 |
+
TSLA,12/2/2025 16:00:00,429.24,-0.002092342028
|
| 908 |
+
TSLA,12/3/2025 16:00:00,446.74,0.04076973255
|
| 909 |
+
TSLA,12/4/2025 16:00:00,454.53,0.01743743564
|
| 910 |
+
TSLA,12/5/2025 16:00:00,455,0.001034035157
|
| 911 |
+
TSLA,12/8/2025 16:00:00,439.58,-0.03389010989
|
| 912 |
+
TSLA,12/9/2025 16:00:00,445.17,0.01271668411
|
| 913 |
+
TSLA,12/10/2025 16:00:00,451.45,0.01410697037
|
| 914 |
+
TSLA,12/11/2025 16:00:00,446.89,-0.01010078636
|
| 915 |
+
TSLA,12/12/2025 16:00:00,458.96,0.02700888362
|
| 916 |
+
TSLA,12/15/2025 16:00:00,475.31,0.03562401952
|
| 917 |
+
TSLA,12/16/2025 16:00:00,489.88,0.03065367865
|
| 918 |
+
TSLA,12/17/2025 16:00:00,467.26,-0.04617457336
|
| 919 |
+
TSLA,12/18/2025 16:00:00,483.37,0.03447759277
|
| 920 |
+
TSLA,12/19/2025 16:00:00,481.2,-0.004489314604
|
| 921 |
+
TSLA,12/22/2025 16:00:00,488.73,0.01564837905
|
| 922 |
+
TSLA,12/23/2025 16:00:00,485.56,-0.006486198924
|
| 923 |
+
TSLA,12/24/2025 13:05:00,485.4,-0.0003295164346
|
| 924 |
+
TSLA,12/26/2025 16:00:00,475.19,-0.0210341986
|
| 925 |
+
TSLA,12/29/2025 16:00:00,459.64,-0.0327237526
|
| 926 |
+
TSLA,12/30/2025 16:00:00,454.43,-0.01133495779
|
| 927 |
+
TSLA,12/31/2025 16:00:00,449.72,-0.01036463262
|
| 928 |
+
TSLA,1/2/2026 16:00:00,438.07,-0.02590500756
|
| 929 |
+
TSLA,1/5/2026 16:00:00,451.67,0.03104526674
|
| 930 |
+
TSLA,1/6/2026 16:00:00,432.96,-0.04142404853
|
| 931 |
+
TSLA,1/7/2026 16:00:00,431.41,-0.003580007391
|
| 932 |
+
TSLA,1/8/2026 16:00:00,435.8,0.01017593473
|
| 933 |
+
TSLA,1/9/2026 16:00:00,445.01,0.0211335475
|
| 934 |
+
TSLA,1/12/2026 16:00:00,448.96,0.008876205029
|
| 935 |
+
TSLA,1/13/2026 16:00:00,447.2,-0.003920171062
|
| 936 |
+
TSLA,1/14/2026 16:00:00,439.2,-0.01788908766
|
| 937 |
+
TSLA,1/15/2026 16:00:00,438.57,-0.00143442623
|
| 938 |
+
TSLA,1/16/2026 16:00:00,437.5,-0.002439747361
|
| 939 |
+
TSLA,1/20/2026 16:00:00,419.25,-0.04171428571
|
| 940 |
+
TSLA,1/21/2026 16:00:00,431.44,0.02907573047
|
| 941 |
+
TSLA,1/22/2026 16:00:00,449.36,0.04153532357
|
| 942 |
+
TSLA,1/23/2026 16:00:00,449.06,-0.0006676161652
|
| 943 |
+
TSLA,1/26/2026 16:00:00,435.2,-0.03086447245
|
| 944 |
+
TSLA,1/27/2026 16:00:00,430.9,-0.009880514706
|
| 945 |
+
TSLA,1/28/2026 16:00:00,431.46,0.001299605477
|
| 946 |
+
TSLA,1/29/2026 16:00:00,416.56,-0.03453390813
|
| 947 |
+
TSLA,1/30/2026 16:00:00,430.41,0.03324851162
|
| 948 |
+
TSLA,2/2/2026 16:00:00,421.81,-0.0199809484
|
| 949 |
+
TSLA,2/3/2026 16:00:00,421.96,0.0003556103459
|
| 950 |
+
TSLA,2/4/2026 16:00:00,406.01,-0.03779979145
|
| 951 |
+
TSLA,2/5/2026 16:00:00,397.21,-0.021674343
|
| 952 |
+
TSLA,2/6/2026 16:00:00,411.11,0.03499408373
|
| 953 |
+
TSLA,2/9/2026 16:00:00,417.32,0.01510544623
|
| 954 |
+
TSLA,2/10/2026 16:00:00,425.21,0.01890635484
|
| 955 |
+
TSLA,2/11/2026 16:00:00,428.27,0.00719644411
|
| 956 |
+
TSLA,2/12/2026 16:00:00,417.07,-0.02615172671
|
| 957 |
+
TSLA,2/13/2026 16:00:00,417.44,0.0008871412473
|
| 958 |
+
TSLA,2/17/2026 16:00:00,410.63,-0.01631372173
|
| 959 |
+
TSLA,2/18/2026 16:00:00,411.32,0.001680344836
|
| 960 |
+
TSLA,2/19/2026 16:00:00,411.71,0.0009481668774
|
| 961 |
+
TSLA,2/20/2026 16:00:00,411.82,0.0002671783537
|
| 962 |
+
TSLA,2/23/2026 16:00:00,399.83,-0.02911466175
|
| 963 |
+
TSLA,2/24/2026 16:00:00,409.38,0.02388515119
|
| 964 |
+
TSLA,2/25/2026 16:00:00,417.4,0.01959060042
|
| 965 |
+
TSLA,2/26/2026 16:00:00,408.58,-0.02113080977
|
| 966 |
+
TSLA,2/27/2026 16:00:00,402.51,-0.01485633169
|
| 967 |
+
TSLA,3/2/2026 16:00:00,403.32,0.002012372363
|
| 968 |
+
TSLA,3/3/2026 16:00:00,392.43,-0.02700089259
|
| 969 |
+
TSLA,3/4/2026 16:00:00,405.94,0.03442652193
|
| 970 |
+
TSLA,3/5/2026 16:00:00,405.55,-0.0009607331133
|
| 971 |
+
TSLA,3/6/2026 16:00:00,396.73,-0.02174824313
|
| 972 |
+
TSLA,3/9/2026 16:00:00,398.68,0.00491518161
|
| 973 |
+
TSLA,3/10/2026 16:00:00,399.24,0.001404635296
|
| 974 |
+
TSLA,3/11/2026 16:00:00,407.82,0.02149083258
|
| 975 |
+
TSLA,3/12/2026 16:00:00,395.01,-0.03141091658
|
| 976 |
+
TSLA,3/13/2026 16:00:00,391.2,-0.009645325435
|
| 977 |
+
TSLA,3/16/2026 16:00:00,395.56,0.01114519427
|
| 978 |
+
TSLA,3/17/2026 16:00:00,399.27,0.0093791081
|
| 979 |
+
TSLA,3/18/2026 16:00:00,392.78,-0.01625466476
|
| 980 |
+
TSLA,3/19/2026 16:00:00,380.3,-0.03177351189
|
| 981 |
+
TSLA,3/20/2026 16:00:00,367.96,-0.03244806732
|
| 982 |
+
TSLA,3/23/2026 16:00:00,380.85,0.03503098163
|
| 983 |
+
TSLA,3/24/2026 16:00:00,383.03,0.005724038335
|
| 984 |
+
TSLA,3/25/2026 16:00:00,385.95,0.007623423753
|
| 985 |
+
TSLA,3/26/2026 16:00:00,372.11,-0.0358595673
|
| 986 |
+
TSLA,3/27/2026 16:00:00,361.83,-0.02762623955
|
| 987 |
+
TSLA,3/30/2026 16:00:00,355.28,-0.01810242379
|
| 988 |
+
TSLA,3/31/2026 16:00:00,371.75,0.0463578023
|
| 989 |
+
TSLA,4/1/2026 16:00:00,381.26,0.02558170814
|
| 990 |
+
TSLA,4/2/2026 16:00:00,360.59,-0.05421497141
|
| 991 |
+
TSLA,4/6/2026 16:00:00,352.82,-0.02154801853
|
| 992 |
+
TSLA,4/7/2026 16:00:00,346.65,-0.01748767077
|
| 993 |
+
TSLA,4/8/2026 16:00:00,343.25,-0.009808163854
|
| 994 |
+
TSLA,4/9/2026 16:00:00,345.62,0.006904588492
|
| 995 |
+
TSLA,4/10/2026 16:00:00,348.95,0.009634859094
|
| 996 |
+
TSLA,4/13/2026 16:00:00,352.42,0.009944118068
|
| 997 |
+
TSLA,4/14/2026 16:00:00,364.2,0.03342602576
|
| 998 |
+
TSLA,4/15/2026 16:00:00,391.95,0.07619439868
|
| 999 |
+
TSLA,4/16/2026 16:00:00,388.9,-0.007781604797
|
| 1000 |
+
TSLA,4/17/2026 16:00:00,400.62,0.03013628182
|
| 1001 |
+
TSLA,4/20/2026 16:00:00,392.5,-0.0202685837
|
| 1002 |
+
TSLA,4/21/2026 16:00:00,386.42,-0.01549044586
|
| 1003 |
+
TSLA,4/22/2026 16:00:00,387.51,0.002820764971
|
| 1004 |
+
TSLA,4/23/2026 16:00:00,373.72,-0.03558617842
|
| 1005 |
+
TSLA,4/24/2026 16:00:00,376.3,0.006903564166
|
| 1006 |
+
TSLA,4/27/2026 16:00:00,378.67,0.006298166357
|
| 1007 |
+
TSLA,4/28/2026 16:00:00,376.02,-0.006998177833
|
| 1008 |
+
TSLA,4/29/2026 16:00:00,372.8,-0.008563374289
|
| 1009 |
+
TSLA,4/30/2026 16:00:00,381.63,0.02368562232
|
| 1010 |
+
TSLA,5/1/2026 16:00:00,390.82,0.04833690987
|
| 1011 |
+
Stock,Date,Close,
|
| 1012 |
+
NVDA,5/2/2025 16:00:00,114.5,
|
| 1013 |
+
NVDA,5/5/2025 16:00:00,113.82,-0.005938864629
|
| 1014 |
+
NVDA,5/6/2025 16:00:00,113.54,-0.0024600246
|
| 1015 |
+
NVDA,5/7/2025 16:00:00,117.06,0.03100228994
|
| 1016 |
+
NVDA,5/8/2025 16:00:00,117.37,0.002648214591
|
| 1017 |
+
NVDA,5/9/2025 16:00:00,116.65,-0.006134446622
|
| 1018 |
+
NVDA,5/12/2025 16:00:00,123,0.05443634805
|
| 1019 |
+
NVDA,5/13/2025 16:00:00,129.93,0.05634146341
|
| 1020 |
+
NVDA,5/14/2025 16:00:00,135.34,0.04163780497
|
| 1021 |
+
NVDA,5/15/2025 16:00:00,134.83,-0.003768287276
|
| 1022 |
+
NVDA,5/16/2025 16:00:00,135.4,0.004227545798
|
| 1023 |
+
NVDA,5/19/2025 16:00:00,135.57,0.001255539143
|
| 1024 |
+
NVDA,5/20/2025 16:00:00,134.38,-0.00877775319
|
| 1025 |
+
NVDA,5/21/2025 16:00:00,131.8,-0.01919928561
|
| 1026 |
+
NVDA,5/22/2025 16:00:00,131.8,0
|
| 1027 |
+
NVDA,5/23/2025 16:00:00,131.29,-0.003869499241
|
| 1028 |
+
NVDA,5/27/2025 16:00:00,135.5,0.03206641785
|
| 1029 |
+
NVDA,5/28/2025 16:00:00,134.81,-0.005092250923
|
| 1030 |
+
NVDA,5/29/2025 16:00:00,139.19,0.03249017135
|
| 1031 |
+
NVDA,5/30/2025 16:00:00,135.13,-0.02916876212
|
| 1032 |
+
NVDA,6/2/2025 16:00:00,137.38,0.01665063272
|
| 1033 |
+
NVDA,6/3/2025 16:00:00,141.22,0.02795166691
|
| 1034 |
+
NVDA,6/4/2025 16:00:00,141.92,0.004956804985
|
| 1035 |
+
NVDA,6/5/2025 16:00:00,139.99,-0.01359921082
|
| 1036 |
+
NVDA,6/6/2025 16:00:00,141.72,0.01235802557
|
| 1037 |
+
NVDA,6/9/2025 16:00:00,142.63,0.006421112052
|
| 1038 |
+
NVDA,6/10/2025 16:00:00,143.96,0.009324826474
|
| 1039 |
+
NVDA,6/11/2025 16:00:00,142.83,-0.007849402612
|
| 1040 |
+
NVDA,6/12/2025 16:00:00,145,0.01519288665
|
| 1041 |
+
NVDA,6/13/2025 16:00:00,141.97,-0.02089655172
|
| 1042 |
+
NVDA,6/16/2025 16:00:00,144.69,0.01915897725
|
| 1043 |
+
NVDA,6/17/2025 16:00:00,144.12,-0.00393945677
|
| 1044 |
+
NVDA,6/18/2025 16:00:00,145.48,0.009436580627
|
| 1045 |
+
NVDA,6/20/2025 16:00:00,143.85,-0.01120428925
|
| 1046 |
+
NVDA,6/23/2025 16:00:00,144.17,0.002224539451
|
| 1047 |
+
NVDA,6/24/2025 16:00:00,147.9,0.02587223417
|
| 1048 |
+
NVDA,6/25/2025 16:00:00,154.31,0.04334009466
|
| 1049 |
+
NVDA,6/26/2025 16:00:00,155.02,0.0046011276
|
| 1050 |
+
NVDA,6/27/2025 16:00:00,157.75,0.01761063089
|
| 1051 |
+
NVDA,6/30/2025 16:00:00,157.99,0.001521394612
|
| 1052 |
+
NVDA,7/1/2025 16:00:00,153.3,-0.02968542313
|
| 1053 |
+
NVDA,7/2/2025 16:00:00,157.25,0.02576647097
|
| 1054 |
+
NVDA,7/3/2025 13:05:00,159.34,0.013290938
|
| 1055 |
+
NVDA,7/7/2025 16:00:00,158.24,-0.006903476842
|
| 1056 |
+
NVDA,7/8/2025 16:00:00,160,0.0111223458
|
| 1057 |
+
NVDA,7/9/2025 16:00:00,162.88,0.018
|
| 1058 |
+
NVDA,7/10/2025 16:00:00,164.1,0.007490176817
|
| 1059 |
+
NVDA,7/11/2025 16:00:00,164.92,0.004996953077
|
| 1060 |
+
NVDA,7/14/2025 16:00:00,164.07,-0.005154014067
|
| 1061 |
+
NVDA,7/15/2025 16:00:00,170.7,0.04040958128
|
| 1062 |
+
NVDA,7/16/2025 16:00:00,171.37,0.003925014646
|
| 1063 |
+
NVDA,7/17/2025 16:00:00,173,0.009511583124
|
| 1064 |
+
NVDA,7/18/2025 16:00:00,172.41,-0.003410404624
|
| 1065 |
+
NVDA,7/21/2025 16:00:00,171.38,-0.005974131431
|
| 1066 |
+
NVDA,7/22/2025 16:00:00,167.03,-0.02538219162
|
| 1067 |
+
NVDA,7/23/2025 16:00:00,170.78,0.0224510567
|
| 1068 |
+
NVDA,7/24/2025 16:00:00,173.74,0.01733224031
|
| 1069 |
+
NVDA,7/25/2025 16:00:00,173.5,-0.001381374468
|
| 1070 |
+
NVDA,7/28/2025 16:00:00,176.75,0.01873198847
|
| 1071 |
+
NVDA,7/29/2025 16:00:00,175.51,-0.007015558699
|
| 1072 |
+
NVDA,7/30/2025 16:00:00,179.27,0.02142328072
|
| 1073 |
+
NVDA,7/31/2025 16:00:00,177.87,-0.007809449434
|
| 1074 |
+
NVDA,8/1/2025 16:00:00,173.72,-0.02333164671
|
| 1075 |
+
NVDA,8/4/2025 16:00:00,180,0.03615012664
|
| 1076 |
+
NVDA,8/5/2025 16:00:00,178.26,-0.009666666667
|
| 1077 |
+
NVDA,8/6/2025 16:00:00,179.42,0.006507348816
|
| 1078 |
+
NVDA,8/7/2025 16:00:00,180.77,0.007524244789
|
| 1079 |
+
NVDA,8/8/2025 16:00:00,182.7,0.01067655031
|
| 1080 |
+
NVDA,8/11/2025 16:00:00,182.06,-0.0035030104
|
| 1081 |
+
NVDA,8/12/2025 16:00:00,183.16,0.006041964188
|
| 1082 |
+
NVDA,8/13/2025 16:00:00,181.59,-0.008571740555
|
| 1083 |
+
NVDA,8/14/2025 16:00:00,182.02,0.002367971805
|
| 1084 |
+
NVDA,8/15/2025 16:00:00,180.45,-0.008625425777
|
| 1085 |
+
NVDA,8/18/2025 16:00:00,182.01,0.008645054032
|
| 1086 |
+
NVDA,8/19/2025 16:00:00,175.64,-0.03499807703
|
| 1087 |
+
NVDA,8/20/2025 16:00:00,175.4,-0.001366431337
|
| 1088 |
+
NVDA,8/21/2025 16:00:00,174.98,-0.002394526796
|
| 1089 |
+
NVDA,8/22/2025 16:00:00,177.99,0.01720196594
|
| 1090 |
+
NVDA,8/25/2025 16:00:00,179.81,0.01022529356
|
| 1091 |
+
NVDA,8/26/2025 16:00:00,181.77,0.01090039486
|
| 1092 |
+
NVDA,8/27/2025 16:00:00,181.6,-0.0009352478407
|
| 1093 |
+
NVDA,8/28/2025 16:00:00,180.17,-0.007874449339
|
| 1094 |
+
NVDA,8/29/2025 16:00:00,174.18,-0.03324637842
|
| 1095 |
+
NVDA,9/2/2025 16:00:00,170.78,-0.01952003674
|
| 1096 |
+
NVDA,9/3/2025 16:00:00,170.62,-0.0009368778545
|
| 1097 |
+
NVDA,9/4/2025 16:00:00,171.66,0.006095416716
|
| 1098 |
+
NVDA,9/5/2025 16:00:00,167.02,-0.02703017593
|
| 1099 |
+
NVDA,9/8/2025 16:00:00,168.31,0.007723625913
|
| 1100 |
+
NVDA,9/9/2025 16:00:00,170.76,0.01455647317
|
| 1101 |
+
NVDA,9/10/2025 16:00:00,177.33,0.03847505271
|
| 1102 |
+
NVDA,9/11/2025 16:00:00,177.17,-0.0009022725991
|
| 1103 |
+
NVDA,9/12/2025 16:00:00,177.82,0.003668792685
|
| 1104 |
+
NVDA,9/15/2025 16:00:00,177.75,-0.0003936565066
|
| 1105 |
+
NVDA,9/16/2025 16:00:00,174.88,-0.01614627286
|
| 1106 |
+
NVDA,9/17/2025 16:00:00,170.29,-0.02624656908
|
| 1107 |
+
NVDA,9/18/2025 16:00:00,176.24,0.0349403958
|
| 1108 |
+
NVDA,9/19/2025 16:00:00,176.67,0.002439854744
|
| 1109 |
+
NVDA,9/22/2025 16:00:00,183.61,0.03928227769
|
| 1110 |
+
NVDA,9/23/2025 16:00:00,178.43,-0.02821197103
|
| 1111 |
+
NVDA,9/24/2025 16:00:00,176.97,-0.008182480525
|
| 1112 |
+
NVDA,9/25/2025 16:00:00,177.69,0.004068486184
|
| 1113 |
+
NVDA,9/26/2025 16:00:00,178.19,0.002813889358
|
| 1114 |
+
NVDA,9/29/2025 16:00:00,181.85,0.02053987317
|
| 1115 |
+
NVDA,9/30/2025 16:00:00,186.58,0.02601044817
|
| 1116 |
+
NVDA,10/1/2025 16:00:00,187.24,0.00353735663
|
| 1117 |
+
NVDA,10/2/2025 16:00:00,188.89,0.008812219611
|
| 1118 |
+
NVDA,10/3/2025 16:00:00,187.62,-0.006723489862
|
| 1119 |
+
NVDA,10/6/2025 16:00:00,185.54,-0.01108623814
|
| 1120 |
+
NVDA,10/7/2025 16:00:00,185.04,-0.002694836693
|
| 1121 |
+
NVDA,10/8/2025 16:00:00,189.11,0.02199524427
|
| 1122 |
+
NVDA,10/9/2025 16:00:00,192.57,0.01829622971
|
| 1123 |
+
NVDA,10/10/2025 16:00:00,183.16,-0.04886534767
|
| 1124 |
+
NVDA,10/13/2025 16:00:00,188.32,0.02817208998
|
| 1125 |
+
NVDA,10/14/2025 16:00:00,180.03,-0.04402081563
|
| 1126 |
+
NVDA,10/15/2025 16:00:00,179.83,-0.001110925957
|
| 1127 |
+
NVDA,10/16/2025 16:00:00,181.81,0.01101039871
|
| 1128 |
+
NVDA,10/17/2025 16:00:00,183.22,0.007755348991
|
| 1129 |
+
NVDA,10/20/2025 16:00:00,182.64,-0.003165593276
|
| 1130 |
+
NVDA,10/21/2025 16:00:00,181.16,-0.008103372755
|
| 1131 |
+
NVDA,10/22/2025 16:00:00,180.28,-0.004857584456
|
| 1132 |
+
NVDA,10/23/2025 16:00:00,182.16,0.01042822276
|
| 1133 |
+
NVDA,10/24/2025 16:00:00,186.26,0.02250768555
|
| 1134 |
+
NVDA,10/27/2025 16:00:00,191.49,0.02807902931
|
| 1135 |
+
NVDA,10/28/2025 16:00:00,201.03,0.04981983393
|
| 1136 |
+
NVDA,10/29/2025 16:00:00,207.04,0.02989603542
|
| 1137 |
+
NVDA,10/30/2025 16:00:00,202.89,-0.02004443586
|
| 1138 |
+
NVDA,10/31/2025 16:00:00,202.49,-0.001971511657
|
| 1139 |
+
NVDA,11/3/2025 16:00:00,206.88,0.02168008297
|
| 1140 |
+
NVDA,11/4/2025 16:00:00,198.69,-0.03958816705
|
| 1141 |
+
NVDA,11/5/2025 16:00:00,195.21,-0.01751472143
|
| 1142 |
+
NVDA,11/6/2025 16:00:00,188.08,-0.0365247682
|
| 1143 |
+
NVDA,11/7/2025 16:00:00,188.15,0.0003721820502
|
| 1144 |
+
NVDA,11/10/2025 16:00:00,199.05,0.05793250066
|
| 1145 |
+
NVDA,11/11/2025 16:00:00,193.16,-0.02959055514
|
| 1146 |
+
NVDA,11/12/2025 16:00:00,193.8,0.003313315386
|
| 1147 |
+
NVDA,11/13/2025 16:00:00,186.86,-0.03581011352
|
| 1148 |
+
NVDA,11/14/2025 16:00:00,190.17,0.01771379643
|
| 1149 |
+
NVDA,11/17/2025 16:00:00,186.6,-0.01877267708
|
| 1150 |
+
NVDA,11/18/2025 16:00:00,181.36,-0.02808145766
|
| 1151 |
+
NVDA,11/19/2025 16:00:00,186.52,0.02845169828
|
| 1152 |
+
NVDA,11/20/2025 16:00:00,180.64,-0.03152476946
|
| 1153 |
+
NVDA,11/21/2025 16:00:00,178.88,-0.009743135518
|
| 1154 |
+
NVDA,11/24/2025 16:00:00,182.55,0.02051654741
|
| 1155 |
+
NVDA,11/25/2025 16:00:00,177.82,-0.02591070939
|
| 1156 |
+
NVDA,11/26/2025 16:00:00,180.26,0.01372174109
|
| 1157 |
+
NVDA,11/28/2025 13:05:00,177,-0.01808498835
|
| 1158 |
+
NVDA,12/1/2025 16:00:00,179.92,0.01649717514
|
| 1159 |
+
NVDA,12/2/2025 16:00:00,181.46,0.008559359715
|
| 1160 |
+
NVDA,12/3/2025 16:00:00,179.59,-0.01030530144
|
| 1161 |
+
NVDA,12/4/2025 16:00:00,183.38,0.02110362492
|
| 1162 |
+
NVDA,12/5/2025 16:00:00,182.41,-0.005289562657
|
| 1163 |
+
NVDA,12/8/2025 16:00:00,185.55,0.01721396853
|
| 1164 |
+
NVDA,12/9/2025 16:00:00,184.97,-0.003125842091
|
| 1165 |
+
NVDA,12/10/2025 16:00:00,183.78,-0.006433475699
|
| 1166 |
+
NVDA,12/11/2025 16:00:00,180.93,-0.01550767222
|
| 1167 |
+
NVDA,12/12/2025 16:00:00,175.02,-0.03266456641
|
| 1168 |
+
NVDA,12/15/2025 16:00:00,176.29,0.007256313564
|
| 1169 |
+
NVDA,12/16/2025 16:00:00,177.72,0.008111634239
|
| 1170 |
+
NVDA,12/17/2025 16:00:00,170.94,-0.03814989872
|
| 1171 |
+
NVDA,12/18/2025 16:00:00,174.14,0.01872001872
|
| 1172 |
+
NVDA,12/19/2025 16:00:00,180.99,0.0393361663
|
| 1173 |
+
NVDA,12/22/2025 16:00:00,183.69,0.01491795127
|
| 1174 |
+
NVDA,12/23/2025 16:00:00,189.21,0.03005062878
|
| 1175 |
+
NVDA,12/24/2025 13:05:00,188.61,-0.003171079753
|
| 1176 |
+
NVDA,12/26/2025 16:00:00,190.53,0.01017973596
|
| 1177 |
+
NVDA,12/29/2025 16:00:00,188.22,-0.01212407495
|
| 1178 |
+
NVDA,12/30/2025 16:00:00,187.54,-0.003612793539
|
| 1179 |
+
NVDA,12/31/2025 16:00:00,186.5,-0.00554548363
|
| 1180 |
+
NVDA,1/2/2026 16:00:00,188.85,0.01260053619
|
| 1181 |
+
NVDA,1/5/2026 16:00:00,188.12,-0.003865501721
|
| 1182 |
+
NVDA,1/6/2026 16:00:00,187.24,-0.004677865192
|
| 1183 |
+
NVDA,1/7/2026 16:00:00,189.11,0.009987182226
|
| 1184 |
+
NVDA,1/8/2026 16:00:00,185.04,-0.02152186558
|
| 1185 |
+
NVDA,1/9/2026 16:00:00,184.86,-0.0009727626459
|
| 1186 |
+
NVDA,1/12/2026 16:00:00,184.94,0.0004327599264
|
| 1187 |
+
NVDA,1/13/2026 16:00:00,185.81,0.004704228398
|
| 1188 |
+
NVDA,1/14/2026 16:00:00,183.14,-0.01436951725
|
| 1189 |
+
NVDA,1/15/2026 16:00:00,187.05,0.02134978705
|
| 1190 |
+
NVDA,1/16/2026 16:00:00,186.23,-0.004383854584
|
| 1191 |
+
NVDA,1/20/2026 16:00:00,178.07,-0.0438167857
|
| 1192 |
+
NVDA,1/21/2026 16:00:00,183.32,0.02948278767
|
| 1193 |
+
NVDA,1/22/2026 16:00:00,184.84,0.00829151211
|
| 1194 |
+
NVDA,1/23/2026 16:00:00,187.67,0.01531053884
|
| 1195 |
+
NVDA,1/26/2026 16:00:00,186.47,-0.00639420259
|
| 1196 |
+
NVDA,1/27/2026 16:00:00,188.52,0.01099372553
|
| 1197 |
+
NVDA,1/28/2026 16:00:00,191.52,0.01591343094
|
| 1198 |
+
NVDA,1/29/2026 16:00:00,192.51,0.005169172932
|
| 1199 |
+
NVDA,1/30/2026 16:00:00,191.13,-0.007168458781
|
| 1200 |
+
NVDA,2/2/2026 16:00:00,185.61,-0.02888086643
|
| 1201 |
+
NVDA,2/3/2026 16:00:00,180.34,-0.02839286676
|
| 1202 |
+
NVDA,2/4/2026 16:00:00,174.19,-0.0341022513
|
| 1203 |
+
NVDA,2/5/2026 16:00:00,171.88,-0.01326138125
|
| 1204 |
+
NVDA,2/6/2026 16:00:00,185.41,0.07871771003
|
| 1205 |
+
NVDA,2/9/2026 16:00:00,190.04,0.02497168438
|
| 1206 |
+
NVDA,2/10/2026 16:00:00,188.54,-0.007893075142
|
| 1207 |
+
NVDA,2/11/2026 16:00:00,190.05,0.008008910576
|
| 1208 |
+
NVDA,2/12/2026 16:00:00,186.94,-0.01636411471
|
| 1209 |
+
NVDA,2/13/2026 16:00:00,182.81,-0.02209265005
|
| 1210 |
+
NVDA,2/17/2026 16:00:00,184.97,0.0118155462
|
| 1211 |
+
NVDA,2/18/2026 16:00:00,187.98,0.01627290912
|
| 1212 |
+
NVDA,2/19/2026 16:00:00,187.9,-0.0004255771891
|
| 1213 |
+
NVDA,2/20/2026 16:00:00,189.82,0.01021820117
|
| 1214 |
+
NVDA,2/23/2026 16:00:00,191.55,0.009113897376
|
| 1215 |
+
NVDA,2/24/2026 16:00:00,192.85,0.006786739755
|
| 1216 |
+
NVDA,2/25/2026 16:00:00,195.56,0.01405237231
|
| 1217 |
+
NVDA,2/26/2026 16:00:00,184.89,-0.05456125997
|
| 1218 |
+
NVDA,2/27/2026 16:00:00,177.19,-0.04164638434
|
| 1219 |
+
NVDA,3/2/2026 16:00:00,182.48,0.02985495795
|
| 1220 |
+
NVDA,3/3/2026 16:00:00,180.05,-0.01331652784
|
| 1221 |
+
NVDA,3/4/2026 16:00:00,183.04,0.01660649819
|
| 1222 |
+
NVDA,3/5/2026 16:00:00,183.34,0.001638986014
|
| 1223 |
+
NVDA,3/6/2026 16:00:00,177.82,-0.03010799607
|
| 1224 |
+
NVDA,3/9/2026 16:00:00,182.65,0.02716229895
|
| 1225 |
+
NVDA,3/10/2026 16:00:00,184.77,0.01160689844
|
| 1226 |
+
NVDA,3/11/2026 16:00:00,186.03,0.006819288846
|
| 1227 |
+
NVDA,3/12/2026 16:00:00,183.14,-0.01553512874
|
| 1228 |
+
NVDA,3/13/2026 16:00:00,180.25,-0.01578027738
|
| 1229 |
+
NVDA,3/16/2026 16:00:00,183.22,0.01647711512
|
| 1230 |
+
NVDA,3/17/2026 16:00:00,181.93,-0.007040716079
|
| 1231 |
+
NVDA,3/18/2026 16:00:00,180.4,-0.008409827956
|
| 1232 |
+
NVDA,3/19/2026 16:00:00,178.56,-0.01019955654
|
| 1233 |
+
NVDA,3/20/2026 16:00:00,172.7,-0.03281810036
|
| 1234 |
+
NVDA,3/23/2026 16:00:00,175.64,0.01702374059
|
| 1235 |
+
NVDA,3/24/2026 16:00:00,175.2,-0.002505124118
|
| 1236 |
+
NVDA,3/25/2026 16:00:00,178.68,0.0198630137
|
| 1237 |
+
NVDA,3/26/2026 16:00:00,171.24,-0.04163868368
|
| 1238 |
+
NVDA,3/27/2026 16:00:00,167.52,-0.02172389629
|
| 1239 |
+
NVDA,3/30/2026 16:00:00,165.17,-0.01402817574
|
| 1240 |
+
NVDA,3/31/2026 16:00:00,174.4,0.05588181873
|
| 1241 |
+
NVDA,4/1/2026 16:00:00,175.75,0.007740825688
|
| 1242 |
+
NVDA,4/2/2026 16:00:00,177.39,0.0093314367
|
| 1243 |
+
NVDA,4/6/2026 16:00:00,177.64,0.001409324088
|
| 1244 |
+
NVDA,4/7/2026 16:00:00,178.1,0.002589506868
|
| 1245 |
+
NVDA,4/8/2026 16:00:00,182.08,0.02234699607
|
| 1246 |
+
NVDA,4/9/2026 16:00:00,183.91,0.01005052724
|
| 1247 |
+
NVDA,4/10/2026 16:00:00,188.63,0.02566472731
|
| 1248 |
+
NVDA,4/13/2026 16:00:00,189.31,0.00360494089
|
| 1249 |
+
NVDA,4/14/2026 16:00:00,196.51,0.03803285616
|
| 1250 |
+
NVDA,4/15/2026 16:00:00,198.87,0.01200956694
|
| 1251 |
+
NVDA,4/16/2026 16:00:00,198.35,-0.00261477347
|
| 1252 |
+
NVDA,4/17/2026 16:00:00,201.68,0.01678850517
|
| 1253 |
+
NVDA,4/20/2026 16:00:00,202.06,0.001884172947
|
| 1254 |
+
NVDA,4/21/2026 16:00:00,199.88,-0.01078887459
|
| 1255 |
+
NVDA,4/22/2026 16:00:00,202.5,0.01310786472
|
| 1256 |
+
NVDA,4/23/2026 16:00:00,199.64,-0.01412345679
|
| 1257 |
+
NVDA,4/24/2026 16:00:00,208.27,0.04322781006
|
| 1258 |
+
NVDA,4/27/2026 16:00:00,216.61,0.04004417343
|
| 1259 |
+
NVDA,4/28/2026 16:00:00,213.17,-0.01588107659
|
| 1260 |
+
NVDA,4/29/2026 16:00:00,209.25,-0.01838907914
|
| 1261 |
+
NVDA,4/30/2026 16:00:00,199.57,-0.046260454
|
| 1262 |
+
NVDA,5/1/2026 16:00:00,198.45,-0.005612065942
|
debug.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import puppeteer from 'puppeteer';
|
| 2 |
+
|
| 3 |
+
(async () => {
|
| 4 |
+
const browser = await puppeteer.launch();
|
| 5 |
+
const page = await browser.newPage();
|
| 6 |
+
|
| 7 |
+
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
|
| 8 |
+
page.on('pageerror', error => console.log('BROWSER ERROR:', error.message));
|
| 9 |
+
page.on('requestfailed', request => console.log('REQUEST FAILED:', request.url(), request.failure().errorText));
|
| 10 |
+
|
| 11 |
+
try {
|
| 12 |
+
await page.goto('http://localhost:5174/');
|
| 13 |
+
// Wait for landing page and click the first option
|
| 14 |
+
await page.waitForSelector('button');
|
| 15 |
+
const buttons = await page.$$('button');
|
| 16 |
+
await buttons[0].click(); // Select General wealth building
|
| 17 |
+
await new Promise(r => setTimeout(r, 500));
|
| 18 |
+
|
| 19 |
+
const buttons2 = await page.$$('button');
|
| 20 |
+
await buttons2[0].click(); // Select I'd see it as a buying opportunity
|
| 21 |
+
await new Promise(r => setTimeout(r, 500));
|
| 22 |
+
|
| 23 |
+
const buttons3 = await page.$$('button');
|
| 24 |
+
await buttons3[0].click(); // Select Cautious
|
| 25 |
+
await new Promise(r => setTimeout(r, 2000));
|
| 26 |
+
|
| 27 |
+
// Now on dashboard, click a stock to open popup
|
| 28 |
+
const qqqBtn = await page.$x("//span[contains(text(), 'QQQ')]");
|
| 29 |
+
if (qqqBtn.length > 0) {
|
| 30 |
+
await qqqBtn[0].click();
|
| 31 |
+
console.log("Clicked QQQ, waiting for crash...");
|
| 32 |
+
} else {
|
| 33 |
+
console.log("QQQ button not found, trying SPY");
|
| 34 |
+
const spyBtn = await page.$x("//span[contains(text(), 'SPY')]");
|
| 35 |
+
if (spyBtn.length > 0) {
|
| 36 |
+
await spyBtn[0].click();
|
| 37 |
+
console.log("Clicked SPY, waiting for crash...");
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
await new Promise(r => setTimeout(r, 5000));
|
| 42 |
+
console.log("Done waiting.");
|
| 43 |
+
} catch (e) {
|
| 44 |
+
console.log("Script error:", e);
|
| 45 |
+
} finally {
|
| 46 |
+
await browser.close();
|
| 47 |
+
}
|
| 48 |
+
})();
|
eslint.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 6 |
+
|
| 7 |
+
export default defineConfig([
|
| 8 |
+
globalIgnores(['dist']),
|
| 9 |
+
{
|
| 10 |
+
files: ['**/*.{js,jsx}'],
|
| 11 |
+
extends: [
|
| 12 |
+
js.configs.recommended,
|
| 13 |
+
reactHooks.configs.flat.recommended,
|
| 14 |
+
reactRefresh.configs.vite,
|
| 15 |
+
],
|
| 16 |
+
languageOptions: {
|
| 17 |
+
globals: globals.browser,
|
| 18 |
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
])
|
index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>gsp</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
nginx.conf
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
server {
|
| 2 |
+
listen 7860;
|
| 3 |
+
server_name localhost;
|
| 4 |
+
|
| 5 |
+
location / {
|
| 6 |
+
root /usr/share/nginx/html;
|
| 7 |
+
index index.html index.htm;
|
| 8 |
+
try_files $uri $uri/ /index.html;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
# Error handling
|
| 12 |
+
error_page 500 502 503 504 /50x.html;
|
| 13 |
+
location = /50x.html {
|
| 14 |
+
root /usr/share/nginx/html;
|
| 15 |
+
}
|
| 16 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "gsp",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@langchain/core": "^1.1.43",
|
| 14 |
+
"@langchain/google-genai": "^2.1.30",
|
| 15 |
+
"@langchain/openai": "^1.4.5",
|
| 16 |
+
"autoprefixer": "^10.5.0",
|
| 17 |
+
"clsx": "^2.1.1",
|
| 18 |
+
"framer-motion": "^12.38.0",
|
| 19 |
+
"lucide-react": "^1.14.0",
|
| 20 |
+
"postcss": "^8.5.13",
|
| 21 |
+
"puppeteer": "^24.42.0",
|
| 22 |
+
"react": "^19.2.5",
|
| 23 |
+
"react-dom": "^19.2.5",
|
| 24 |
+
"react-router-dom": "^7.14.2",
|
| 25 |
+
"recharts": "^3.8.1",
|
| 26 |
+
"tailwind-merge": "^3.5.0",
|
| 27 |
+
"tailwindcss": "^3.4.19"
|
| 28 |
+
},
|
| 29 |
+
"devDependencies": {
|
| 30 |
+
"@eslint/js": "^10.0.1",
|
| 31 |
+
"@types/react": "^19.2.14",
|
| 32 |
+
"@types/react-dom": "^19.2.3",
|
| 33 |
+
"@vitejs/plugin-react": "^4.7.0",
|
| 34 |
+
"eslint": "^10.2.1",
|
| 35 |
+
"eslint-plugin-react-hooks": "^7.1.1",
|
| 36 |
+
"eslint-plugin-react-refresh": "^0.5.2",
|
| 37 |
+
"globals": "^17.5.0",
|
| 38 |
+
"vite": "^5.4.21"
|
| 39 |
+
}
|
| 40 |
+
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
public/favicon.svg
ADDED
|
|
Git LFS Details
|
public/icons.svg
ADDED
|
|
Git LFS Details
|
public/port-satellite.png
ADDED
|
Git LFS Details
|
src/App.css
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.counter {
|
| 2 |
+
font-size: 16px;
|
| 3 |
+
padding: 5px 10px;
|
| 4 |
+
border-radius: 5px;
|
| 5 |
+
color: var(--accent);
|
| 6 |
+
background: var(--accent-bg);
|
| 7 |
+
border: 2px solid transparent;
|
| 8 |
+
transition: border-color 0.3s;
|
| 9 |
+
margin-bottom: 24px;
|
| 10 |
+
|
| 11 |
+
&:hover {
|
| 12 |
+
border-color: var(--accent-border);
|
| 13 |
+
}
|
| 14 |
+
&:focus-visible {
|
| 15 |
+
outline: 2px solid var(--accent);
|
| 16 |
+
outline-offset: 2px;
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.hero {
|
| 21 |
+
position: relative;
|
| 22 |
+
|
| 23 |
+
.base,
|
| 24 |
+
.framework,
|
| 25 |
+
.vite {
|
| 26 |
+
inset-inline: 0;
|
| 27 |
+
margin: 0 auto;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.base {
|
| 31 |
+
width: 170px;
|
| 32 |
+
position: relative;
|
| 33 |
+
z-index: 0;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.framework,
|
| 37 |
+
.vite {
|
| 38 |
+
position: absolute;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.framework {
|
| 42 |
+
z-index: 1;
|
| 43 |
+
top: 34px;
|
| 44 |
+
height: 28px;
|
| 45 |
+
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
| 46 |
+
scale(1.4);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.vite {
|
| 50 |
+
z-index: 0;
|
| 51 |
+
top: 107px;
|
| 52 |
+
height: 26px;
|
| 53 |
+
width: auto;
|
| 54 |
+
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
| 55 |
+
scale(0.8);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
#center {
|
| 60 |
+
display: flex;
|
| 61 |
+
flex-direction: column;
|
| 62 |
+
gap: 25px;
|
| 63 |
+
place-content: center;
|
| 64 |
+
place-items: center;
|
| 65 |
+
flex-grow: 1;
|
| 66 |
+
|
| 67 |
+
@media (max-width: 1024px) {
|
| 68 |
+
padding: 32px 20px 24px;
|
| 69 |
+
gap: 18px;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#next-steps {
|
| 74 |
+
display: flex;
|
| 75 |
+
border-top: 1px solid var(--border);
|
| 76 |
+
text-align: left;
|
| 77 |
+
|
| 78 |
+
& > div {
|
| 79 |
+
flex: 1 1 0;
|
| 80 |
+
padding: 32px;
|
| 81 |
+
@media (max-width: 1024px) {
|
| 82 |
+
padding: 24px 20px;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.icon {
|
| 87 |
+
margin-bottom: 16px;
|
| 88 |
+
width: 22px;
|
| 89 |
+
height: 22px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@media (max-width: 1024px) {
|
| 93 |
+
flex-direction: column;
|
| 94 |
+
text-align: center;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
#docs {
|
| 99 |
+
border-right: 1px solid var(--border);
|
| 100 |
+
|
| 101 |
+
@media (max-width: 1024px) {
|
| 102 |
+
border-right: none;
|
| 103 |
+
border-bottom: 1px solid var(--border);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
#next-steps ul {
|
| 108 |
+
list-style: none;
|
| 109 |
+
padding: 0;
|
| 110 |
+
display: flex;
|
| 111 |
+
gap: 8px;
|
| 112 |
+
margin: 32px 0 0;
|
| 113 |
+
|
| 114 |
+
.logo {
|
| 115 |
+
height: 18px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
a {
|
| 119 |
+
color: var(--text-h);
|
| 120 |
+
font-size: 16px;
|
| 121 |
+
border-radius: 6px;
|
| 122 |
+
background: var(--social-bg);
|
| 123 |
+
display: flex;
|
| 124 |
+
padding: 6px 12px;
|
| 125 |
+
align-items: center;
|
| 126 |
+
gap: 8px;
|
| 127 |
+
text-decoration: none;
|
| 128 |
+
transition: box-shadow 0.3s;
|
| 129 |
+
|
| 130 |
+
&:hover {
|
| 131 |
+
box-shadow: var(--shadow);
|
| 132 |
+
}
|
| 133 |
+
.button-icon {
|
| 134 |
+
height: 18px;
|
| 135 |
+
width: 18px;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
@media (max-width: 1024px) {
|
| 140 |
+
margin-top: 20px;
|
| 141 |
+
flex-wrap: wrap;
|
| 142 |
+
justify-content: center;
|
| 143 |
+
|
| 144 |
+
li {
|
| 145 |
+
flex: 1 1 calc(50% - 8px);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
a {
|
| 149 |
+
width: 100%;
|
| 150 |
+
justify-content: center;
|
| 151 |
+
box-sizing: border-box;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
#spacer {
|
| 157 |
+
height: 88px;
|
| 158 |
+
border-top: 1px solid var(--border);
|
| 159 |
+
@media (max-width: 1024px) {
|
| 160 |
+
height: 48px;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.ticks {
|
| 165 |
+
position: relative;
|
| 166 |
+
width: 100%;
|
| 167 |
+
|
| 168 |
+
&::before,
|
| 169 |
+
&::after {
|
| 170 |
+
content: '';
|
| 171 |
+
position: absolute;
|
| 172 |
+
top: -4.5px;
|
| 173 |
+
border: 5px solid transparent;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
&::before {
|
| 177 |
+
left: 0;
|
| 178 |
+
border-left-color: var(--border);
|
| 179 |
+
}
|
| 180 |
+
&::after {
|
| 181 |
+
right: 0;
|
| 182 |
+
border-right-color: var(--border);
|
| 183 |
+
}
|
| 184 |
+
}
|
src/App.jsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
| 3 |
+
import LandingPage from './pages/LandingPage';
|
| 4 |
+
import Dashboard from './pages/Dashboard';
|
| 5 |
+
|
| 6 |
+
function App() {
|
| 7 |
+
const [riskProfile, setRiskProfile] = useState(() => {
|
| 8 |
+
return localStorage.getItem('gs_risk_profile') || null;
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
const handleSetRiskProfile = (profile) => {
|
| 12 |
+
setRiskProfile(profile);
|
| 13 |
+
localStorage.setItem('gs_risk_profile', profile);
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<Router>
|
| 18 |
+
<div className="min-h-screen bg-gs-light text-gs-navy font-sans">
|
| 19 |
+
<Routes>
|
| 20 |
+
<Route
|
| 21 |
+
path="/"
|
| 22 |
+
element={<LandingPage setRiskProfile={handleSetRiskProfile} />}
|
| 23 |
+
/>
|
| 24 |
+
<Route
|
| 25 |
+
path="/dashboard"
|
| 26 |
+
element={riskProfile ? <Dashboard riskProfile={riskProfile} /> : <Navigate to="/" />}
|
| 27 |
+
/>
|
| 28 |
+
</Routes>
|
| 29 |
+
</div>
|
| 30 |
+
</Router>
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export default App;
|
src/assets/hero.png
ADDED
|
Git LFS Details
|
src/assets/react.svg
ADDED
|
|
Git LFS Details
|
src/assets/vite.svg
ADDED
|
|
Git LFS Details
|
src/components/FeeTransparencyModule.jsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Search } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
export default function FeeTransparencyModule({ portfolio }) {
|
| 5 |
+
const { hiddenFees } = portfolio;
|
| 6 |
+
|
| 7 |
+
// Convert percentages to an illustrative dollar amount based on a $10,000 investment over 1 year
|
| 8 |
+
const illustrativeInvestment = 10000;
|
| 9 |
+
const expenseRatioCost = (illustrativeInvestment * (hiddenFees.expenseRatio / 100)).toFixed(2);
|
| 10 |
+
const advisoryCost = (illustrativeInvestment * (hiddenFees.advisoryFee / 100)).toFixed(2);
|
| 11 |
+
const totalCost = (parseFloat(expenseRatioCost) + parseFloat(advisoryCost) + hiddenFees.tradingCosts).toFixed(2);
|
| 12 |
+
|
| 13 |
+
return (
|
| 14 |
+
<div className="bg-gs-navy text-white rounded-2xl p-8 shadow-lg relative overflow-hidden">
|
| 15 |
+
{/* Background Accent */}
|
| 16 |
+
<div className="absolute -right-10 -top-10 w-40 h-40 bg-white/5 rounded-full blur-2xl"></div>
|
| 17 |
+
|
| 18 |
+
<div className="relative z-10">
|
| 19 |
+
<header className="mb-6 flex justify-between items-end border-b border-white/10 pb-4">
|
| 20 |
+
<div>
|
| 21 |
+
<h2 className="text-2xl font-light mb-1 flex items-center">
|
| 22 |
+
<Search className="mr-2 text-gs-gold" size={24} /> Radical Transparency
|
| 23 |
+
</h2>
|
| 24 |
+
<p className="text-white/60 text-sm font-light">What you actually pay per $10,000 invested yearly</p>
|
| 25 |
+
</div>
|
| 26 |
+
<div className="text-right">
|
| 27 |
+
<span className="text-xs uppercase tracking-widest text-gs-gold font-semibold block mb-1">Fee Rating</span>
|
| 28 |
+
<span className="text-lg font-medium">{portfolio.feeImpact}</span>
|
| 29 |
+
</div>
|
| 30 |
+
</header>
|
| 31 |
+
|
| 32 |
+
<div className="space-y-4 font-light text-sm">
|
| 33 |
+
<div className="flex justify-between items-center bg-white/5 p-3 rounded-lg">
|
| 34 |
+
<span className="text-white/80">Fund Expense Ratios ({hiddenFees.expenseRatio}%)</span>
|
| 35 |
+
<span className="font-medium text-white">${expenseRatioCost}</span>
|
| 36 |
+
</div>
|
| 37 |
+
<div className="flex justify-between items-center bg-white/5 p-3 rounded-lg">
|
| 38 |
+
<span className="text-white/80">Platform/Advisory Fee ({hiddenFees.advisoryFee}%)</span>
|
| 39 |
+
<span className="font-medium text-white">${advisoryCost}</span>
|
| 40 |
+
</div>
|
| 41 |
+
<div className="flex justify-between items-center bg-white/5 p-3 rounded-lg">
|
| 42 |
+
<span className="text-white/80">Estimated Trading Costs</span>
|
| 43 |
+
<span className="font-medium text-white">${hiddenFees.tradingCosts.toFixed(2)}</span>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div className="flex justify-between items-center pt-4 border-t border-white/10">
|
| 47 |
+
<span className="font-medium">Total Yearly Cost</span>
|
| 48 |
+
<span className="text-xl font-medium text-gs-gold">${totalCost}</span>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<p className="text-xs text-white/40 mt-6 italic">
|
| 53 |
+
*Many traditional brokerages hide these numbers in dense prospectuses. We show them to you upfront so there are no surprises.
|
| 54 |
+
</p>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
);
|
| 58 |
+
}
|
src/components/FinancialCalculators.jsx
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useMemo } from 'react';
|
| 2 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
+
import { TrendingUp, Wallet, Landmark, ArrowRight, X, Info, Calculator, Percent, ShieldCheck } from 'lucide-react';
|
| 4 |
+
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
| 5 |
+
|
| 6 |
+
const CALCULATORS = [
|
| 7 |
+
{
|
| 8 |
+
id: 'compound',
|
| 9 |
+
tag: 'INVESTING',
|
| 10 |
+
title: 'Compound Interest Calculator',
|
| 11 |
+
description: 'Watch your money grow year by year with a live animated chart. Adjust return rate, contributions, and timeline.',
|
| 12 |
+
icon: <TrendingUp className="text-blue-500" />,
|
| 13 |
+
color: 'border-blue-400',
|
| 14 |
+
features: [
|
| 15 |
+
'Year-by-year growth chart',
|
| 16 |
+
'Contribution vs. gain breakdown',
|
| 17 |
+
'Rule of 72 insight built in',
|
| 18 |
+
'Compare fee drag scenarios'
|
| 19 |
+
]
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
id: 'retirement',
|
| 23 |
+
tag: 'RETIREMENT',
|
| 24 |
+
title: 'Retirement Savings Calculator',
|
| 25 |
+
description: 'Find your magic number — how much you need to retire, based on your spending, Social Security, and withdrawal rate.',
|
| 26 |
+
icon: <Landmark className="text-amber-500" />,
|
| 27 |
+
color: 'border-amber-400',
|
| 28 |
+
features: [
|
| 29 |
+
'4% rule & safe withdrawal modeling',
|
| 30 |
+
'Social Security income offset',
|
| 31 |
+
'On-track vs. gap analysis',
|
| 32 |
+
'Inflation-adjusted projections'
|
| 33 |
+
]
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
id: 'tax',
|
| 37 |
+
tag: 'TAX STRATEGY',
|
| 38 |
+
title: 'Roth vs. Traditional IRA',
|
| 39 |
+
description: 'Side-by-side after-tax comparison at retirement. Select your current and expected future tax brackets.',
|
| 40 |
+
icon: <Wallet className="text-purple-500" />,
|
| 41 |
+
color: 'border-purple-400',
|
| 42 |
+
features: [
|
| 43 |
+
'Current vs. retirement bracket selector',
|
| 44 |
+
'After-tax value comparison',
|
| 45 |
+
'Break-even tax rate shown',
|
| 46 |
+
'RMD impact explained'
|
| 47 |
+
]
|
| 48 |
+
}
|
| 49 |
+
];
|
| 50 |
+
|
| 51 |
+
export default function FinancialCalculators() {
|
| 52 |
+
const [activeCalc, setActiveCalc] = useState(null);
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
<div className="mt-12 mb-16">
|
| 56 |
+
<div className="flex items-center justify-between mb-8">
|
| 57 |
+
<div>
|
| 58 |
+
<h2 className="text-2xl font-light text-gs-navy">Institutional <span className="font-semibold">Planning Tools</span></h2>
|
| 59 |
+
<p className="text-gs-slate text-sm">Advanced modeling used by our private wealth advisors.</p>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 64 |
+
{CALCULATORS.map((calc) => (
|
| 65 |
+
<CalculatorCard
|
| 66 |
+
key={calc.id}
|
| 67 |
+
calc={calc}
|
| 68 |
+
onClick={() => setActiveCalc(calc)}
|
| 69 |
+
/>
|
| 70 |
+
))}
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<AnimatePresence>
|
| 74 |
+
{activeCalc && (
|
| 75 |
+
<CalculatorModal
|
| 76 |
+
calc={activeCalc}
|
| 77 |
+
onClose={() => setActiveCalc(null)}
|
| 78 |
+
/>
|
| 79 |
+
)}
|
| 80 |
+
</AnimatePresence>
|
| 81 |
+
</div>
|
| 82 |
+
);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function CalculatorCard({ calc, onClick }) {
|
| 86 |
+
return (
|
| 87 |
+
<motion.div
|
| 88 |
+
whileHover={{ y: -5 }}
|
| 89 |
+
className={`bg-white rounded-xl shadow-sm border-t-4 ${calc.color} p-6 flex flex-col h-full border-x border-b border-gray-100 hover:shadow-md transition-shadow cursor-pointer group`}
|
| 90 |
+
onClick={onClick}
|
| 91 |
+
>
|
| 92 |
+
<div className="flex justify-between items-start mb-6">
|
| 93 |
+
<div className="p-3 bg-gray-50 rounded-xl group-hover:bg-gs-light transition-colors">
|
| 94 |
+
{calc.icon}
|
| 95 |
+
</div>
|
| 96 |
+
<span className="text-[10px] font-bold tracking-widest text-gs-slate/60 bg-gray-100 px-2 py-1 rounded">
|
| 97 |
+
{calc.tag}
|
| 98 |
+
</span>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<h3 className="text-xl font-semibold text-gs-navy mb-3 group-hover:text-gs-gold transition-colors">
|
| 102 |
+
{calc.title}
|
| 103 |
+
</h3>
|
| 104 |
+
|
| 105 |
+
<p className="text-gs-slate text-sm font-light leading-relaxed mb-6 flex-grow">
|
| 106 |
+
{calc.description}
|
| 107 |
+
</p>
|
| 108 |
+
|
| 109 |
+
<ul className="space-y-2 mb-8">
|
| 110 |
+
{calc.features.map((feature, i) => (
|
| 111 |
+
<li key={i} className="flex items-center text-xs text-gs-slate/80">
|
| 112 |
+
<ShieldCheck size={14} className="text-gs-gold mr-2 flex-shrink-0" />
|
| 113 |
+
{feature}
|
| 114 |
+
</li>
|
| 115 |
+
))}
|
| 116 |
+
</ul>
|
| 117 |
+
|
| 118 |
+
<div className="flex items-center text-gs-navy font-semibold text-sm group-hover:translate-x-1 transition-transform">
|
| 119 |
+
Open Calculator <ArrowRight size={16} className="ml-2" />
|
| 120 |
+
</div>
|
| 121 |
+
</motion.div>
|
| 122 |
+
);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function CalculatorModal({ calc, onClose }) {
|
| 126 |
+
const [inputs, setInputs] = useState({
|
| 127 |
+
salary: 120000,
|
| 128 |
+
contribution: 15, // percent
|
| 129 |
+
has401k: true,
|
| 130 |
+
employerMatch: 4,
|
| 131 |
+
age: 30,
|
| 132 |
+
retirementAge: 65,
|
| 133 |
+
initialSavings: 50000,
|
| 134 |
+
annualReturn: 7,
|
| 135 |
+
taxBracket: 24,
|
| 136 |
+
futureTaxBracket: 20
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
const handleInputChange = (e) => {
|
| 140 |
+
const { name, value, type, checked } = e.target;
|
| 141 |
+
setInputs(prev => ({
|
| 142 |
+
...prev,
|
| 143 |
+
[name]: type === 'checkbox' ? checked : parseFloat(value)
|
| 144 |
+
}));
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
const renderCalculatorContent = () => {
|
| 148 |
+
switch (calc.id) {
|
| 149 |
+
case 'compound':
|
| 150 |
+
return <CompoundCalc inputs={inputs} onChange={handleInputChange} />;
|
| 151 |
+
case 'retirement':
|
| 152 |
+
return <RetirementCalc inputs={inputs} onChange={handleInputChange} />;
|
| 153 |
+
case 'tax':
|
| 154 |
+
return <TaxCalc inputs={inputs} onChange={handleInputChange} />;
|
| 155 |
+
default:
|
| 156 |
+
return null;
|
| 157 |
+
}
|
| 158 |
+
};
|
| 159 |
+
|
| 160 |
+
return (
|
| 161 |
+
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gs-navy/40 backdrop-blur-sm">
|
| 162 |
+
<motion.div
|
| 163 |
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 164 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 165 |
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 166 |
+
className="bg-white rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col md:flex-row"
|
| 167 |
+
>
|
| 168 |
+
{/* Sidebar / Inputs */}
|
| 169 |
+
<div className="w-full md:w-80 bg-gs-light p-8 border-r border-gray-200 overflow-y-auto">
|
| 170 |
+
<div className="flex items-center mb-8">
|
| 171 |
+
<div className="p-2 bg-white rounded-lg shadow-sm mr-3">
|
| 172 |
+
{calc.icon}
|
| 173 |
+
</div>
|
| 174 |
+
<h3 className="font-bold text-gs-navy uppercase tracking-wider text-sm">{calc.tag}</h3>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<div className="space-y-6">
|
| 178 |
+
<div className="grid grid-cols-2 gap-4">
|
| 179 |
+
<InputGroup
|
| 180 |
+
label="Current Age"
|
| 181 |
+
name="age"
|
| 182 |
+
value={inputs.age}
|
| 183 |
+
onChange={handleInputChange}
|
| 184 |
+
/>
|
| 185 |
+
<InputGroup
|
| 186 |
+
label="Retire Age"
|
| 187 |
+
name="retirementAge"
|
| 188 |
+
value={inputs.retirementAge}
|
| 189 |
+
onChange={handleInputChange}
|
| 190 |
+
/>
|
| 191 |
+
</div>
|
| 192 |
+
<InputGroup
|
| 193 |
+
label="Annual Salary"
|
| 194 |
+
name="salary"
|
| 195 |
+
value={inputs.salary}
|
| 196 |
+
onChange={handleInputChange}
|
| 197 |
+
prefix="$"
|
| 198 |
+
/>
|
| 199 |
+
<InputGroup
|
| 200 |
+
label="401k Contribution (%)"
|
| 201 |
+
name="contribution"
|
| 202 |
+
value={inputs.contribution}
|
| 203 |
+
onChange={handleInputChange}
|
| 204 |
+
suffix="%"
|
| 205 |
+
type="range"
|
| 206 |
+
min="0"
|
| 207 |
+
max="50"
|
| 208 |
+
/>
|
| 209 |
+
<InputGroup
|
| 210 |
+
label="Employer Match (%)"
|
| 211 |
+
name="employerMatch"
|
| 212 |
+
value={inputs.employerMatch}
|
| 213 |
+
onChange={handleInputChange}
|
| 214 |
+
suffix="%"
|
| 215 |
+
/>
|
| 216 |
+
<div className="grid grid-cols-2 gap-4">
|
| 217 |
+
<InputGroup
|
| 218 |
+
label="Current Tax"
|
| 219 |
+
name="taxBracket"
|
| 220 |
+
value={inputs.taxBracket}
|
| 221 |
+
onChange={handleInputChange}
|
| 222 |
+
suffix="%"
|
| 223 |
+
/>
|
| 224 |
+
<InputGroup
|
| 225 |
+
label="Retire Tax"
|
| 226 |
+
name="futureTaxBracket"
|
| 227 |
+
value={inputs.futureTaxBracket}
|
| 228 |
+
onChange={handleInputChange}
|
| 229 |
+
suffix="%"
|
| 230 |
+
/>
|
| 231 |
+
</div>
|
| 232 |
+
<InputGroup
|
| 233 |
+
label="Initial Savings"
|
| 234 |
+
name="initialSavings"
|
| 235 |
+
value={inputs.initialSavings}
|
| 236 |
+
onChange={handleInputChange}
|
| 237 |
+
prefix="$"
|
| 238 |
+
/>
|
| 239 |
+
<InputGroup
|
| 240 |
+
label="Expected Return"
|
| 241 |
+
name="annualReturn"
|
| 242 |
+
value={inputs.annualReturn}
|
| 243 |
+
onChange={handleInputChange}
|
| 244 |
+
suffix="%"
|
| 245 |
+
/>
|
| 246 |
+
</div>
|
| 247 |
+
|
| 248 |
+
<div className="mt-12 p-4 bg-gs-gold/10 rounded-xl border border-gs-gold/20">
|
| 249 |
+
<h4 className="text-xs font-bold text-gs-gold mb-2 flex items-center">
|
| 250 |
+
<Info size={14} className="mr-1" /> GS ADVICE
|
| 251 |
+
</h4>
|
| 252 |
+
<InvestmentAdvice inputs={inputs} />
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
{/* Main Content / Results */}
|
| 257 |
+
<div className="flex-grow flex flex-col h-full bg-white relative">
|
| 258 |
+
<button
|
| 259 |
+
onClick={onClose}
|
| 260 |
+
className="absolute top-6 right-6 p-2 hover:bg-gray-100 rounded-full transition-colors z-10"
|
| 261 |
+
>
|
| 262 |
+
<X size={20} className="text-gs-slate" />
|
| 263 |
+
</button>
|
| 264 |
+
|
| 265 |
+
<div className="p-8 md:p-12 overflow-y-auto">
|
| 266 |
+
<div className="mb-10">
|
| 267 |
+
<h2 className="text-3xl font-light text-gs-navy mb-2">{calc.title}</h2>
|
| 268 |
+
<p className="text-gs-slate font-light">Interactive projection based on institutional modeling.</p>
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
{renderCalculatorContent()}
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
</motion.div>
|
| 275 |
+
</div>
|
| 276 |
+
);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
function InputGroup({ label, name, value, onChange, prefix, suffix, type = "number", min, max }) {
|
| 280 |
+
return (
|
| 281 |
+
<div className="space-y-2">
|
| 282 |
+
<div className="flex justify-between items-center">
|
| 283 |
+
<label className="text-xs font-semibold text-gs-slate uppercase tracking-tighter">{label}</label>
|
| 284 |
+
{type === "range" && <span className="text-sm font-bold text-gs-navy">{value}{suffix}</span>}
|
| 285 |
+
</div>
|
| 286 |
+
<div className="relative">
|
| 287 |
+
{prefix && <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">{prefix}</span>}
|
| 288 |
+
<input
|
| 289 |
+
type={type}
|
| 290 |
+
name={name}
|
| 291 |
+
value={value}
|
| 292 |
+
onChange={onChange}
|
| 293 |
+
min={min}
|
| 294 |
+
max={max}
|
| 295 |
+
className={`w-full bg-white border border-gray-200 rounded-lg p-2 text-sm focus:ring-2 focus:ring-gs-gold focus:border-transparent outline-none transition-all ${prefix ? 'pl-7' : ''} ${suffix && type !== 'range' ? 'pr-7' : ''}`}
|
| 296 |
+
/>
|
| 297 |
+
{suffix && type !== "range" && <span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">{suffix}</span>}
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
function InvestmentAdvice({ inputs }) {
|
| 304 |
+
const { salary, contribution, employerMatch } = inputs;
|
| 305 |
+
const totalContribution = contribution + employerMatch;
|
| 306 |
+
|
| 307 |
+
let advice = "";
|
| 308 |
+
if (totalContribution < 15) {
|
| 309 |
+
advice = `Increase your total savings to at least 15% ($${(salary * 0.15 / 12).toLocaleString()} /mo) to remain on track for institutional-grade retirement.`;
|
| 310 |
+
} else if (contribution > employerMatch + 10) {
|
| 311 |
+
advice = "You are over-contributing to 401k. Consider diversifying into a brokerage account for liquidity or high-yield GS instruments.";
|
| 312 |
+
} else {
|
| 313 |
+
advice = "Excellent contribution level. Ensure your portfolio allocation matches your risk profile in the main dashboard.";
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
return <p className="text-xs text-gs-navy leading-relaxed">{advice}</p>;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
// Calculator Logic Components
|
| 320 |
+
function CompoundCalc({ inputs }) {
|
| 321 |
+
const data = useMemo(() => {
|
| 322 |
+
let current = inputs.initialSavings;
|
| 323 |
+
let totalContributed = inputs.initialSavings;
|
| 324 |
+
const yearlyReturn = inputs.annualReturn / 100;
|
| 325 |
+
const yearlyContribution = (inputs.salary * (inputs.contribution / 100));
|
| 326 |
+
|
| 327 |
+
const results = [];
|
| 328 |
+
for (let i = 0; i <= 30; i++) {
|
| 329 |
+
results.push({
|
| 330 |
+
year: `Year ${i}`,
|
| 331 |
+
balance: Math.round(current),
|
| 332 |
+
contributions: Math.round(totalContributed)
|
| 333 |
+
});
|
| 334 |
+
current = (current + yearlyContribution) * (1 + yearlyReturn);
|
| 335 |
+
totalContributed += yearlyContribution;
|
| 336 |
+
}
|
| 337 |
+
return results;
|
| 338 |
+
}, [inputs]);
|
| 339 |
+
|
| 340 |
+
const finalBalance = data[data.length - 1].balance;
|
| 341 |
+
|
| 342 |
+
return (
|
| 343 |
+
<div className="space-y-8">
|
| 344 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 345 |
+
<div className="p-6 bg-gs-navy text-white rounded-2xl">
|
| 346 |
+
<p className="text-xs font-bold text-gs-gold uppercase tracking-widest mb-1">Estimated Value (30 Years)</p>
|
| 347 |
+
<p className="text-4xl font-light">${finalBalance.toLocaleString()}</p>
|
| 348 |
+
</div>
|
| 349 |
+
<div className="p-6 bg-gs-light rounded-2xl border border-gray-100">
|
| 350 |
+
<p className="text-xs font-bold text-gs-slate uppercase tracking-widest mb-1">Total Interest Earned</p>
|
| 351 |
+
<p className="text-4xl font-light text-gs-navy">${(finalBalance - data[data.length-1].contributions).toLocaleString()}</p>
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
|
| 355 |
+
<div className="h-[350px] w-full">
|
| 356 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 357 |
+
<AreaChart data={data}>
|
| 358 |
+
<defs>
|
| 359 |
+
<linearGradient id="colorBalance" x1="0" y1="0" x2="0" y2="1">
|
| 360 |
+
<stop offset="5%" stopColor="#C5A880" stopOpacity={0.3}/>
|
| 361 |
+
<stop offset="95%" stopColor="#C5A880" stopOpacity={0}/>
|
| 362 |
+
</linearGradient>
|
| 363 |
+
</defs>
|
| 364 |
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#eee" />
|
| 365 |
+
<XAxis dataKey="year" hide />
|
| 366 |
+
<YAxis
|
| 367 |
+
tickFormatter={(value) => `$${value / 1000}k`}
|
| 368 |
+
axisLine={false}
|
| 369 |
+
tickLine={false}
|
| 370 |
+
tick={{fontSize: 12, fill: '#666'}}
|
| 371 |
+
/>
|
| 372 |
+
<Tooltip
|
| 373 |
+
formatter={(value) => [`$${value.toLocaleString()}`, '']}
|
| 374 |
+
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' }}
|
| 375 |
+
/>
|
| 376 |
+
<Area type="monotone" dataKey="balance" stroke="#C5A880" strokeWidth={3} fillOpacity={1} fill="url(#colorBalance)" />
|
| 377 |
+
<Area type="monotone" dataKey="contributions" stroke="#0B233F" strokeWidth={2} fillOpacity={0.1} fill="#0B233F" />
|
| 378 |
+
</AreaChart>
|
| 379 |
+
</ResponsiveContainer>
|
| 380 |
+
</div>
|
| 381 |
+
</div>
|
| 382 |
+
);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
function RetirementCalc({ inputs }) {
|
| 386 |
+
const { salary, contribution, employerMatch, initialSavings, annualReturn, age, retirementAge } = inputs;
|
| 387 |
+
|
| 388 |
+
const yearsToRetirement = retirementAge - age;
|
| 389 |
+
const annualContribution = salary * ((contribution + employerMatch) / 100);
|
| 390 |
+
const rate = annualReturn / 100;
|
| 391 |
+
|
| 392 |
+
// FV of current savings + FV of annual contributions
|
| 393 |
+
const fvInitial = initialSavings * Math.pow(1 + rate, yearsToRetirement);
|
| 394 |
+
const fvContributions = annualContribution * (Math.pow(1 + rate, yearsToRetirement) - 1) / rate;
|
| 395 |
+
const projectedTotal = fvInitial + fvContributions;
|
| 396 |
+
|
| 397 |
+
// Retirement Need: 80% of salary * 25 (4% rule)
|
| 398 |
+
const annualNeed = salary * 0.8;
|
| 399 |
+
const totalNeed = annualNeed * 25;
|
| 400 |
+
const gap = totalNeed - projectedTotal;
|
| 401 |
+
|
| 402 |
+
return (
|
| 403 |
+
<div className="space-y-6">
|
| 404 |
+
<div className="p-8 bg-gs-light rounded-2xl border border-dashed border-gs-gold/50 flex flex-col items-center justify-center text-center">
|
| 405 |
+
<Landmark size={48} className="text-gs-gold mb-4" />
|
| 406 |
+
<h4 className="text-xl font-medium text-gs-navy mb-2">Retirement Readiness Analysis</h4>
|
| 407 |
+
<p className="text-gs-slate max-w-md">
|
| 408 |
+
To maintain your lifestyle, you'll need approximately <span className="font-bold text-gs-navy">${(totalNeed / 1000000).toFixed(1)}M</span>.
|
| 409 |
+
Your current path projects a fund of <span className="font-bold text-gs-navy">${(projectedTotal / 1000000).toFixed(1)}M</span> in {yearsToRetirement} years.
|
| 410 |
+
</p>
|
| 411 |
+
</div>
|
| 412 |
+
|
| 413 |
+
<div className="grid grid-cols-3 gap-4">
|
| 414 |
+
<div className="p-4 bg-white border border-gray-100 rounded-xl shadow-sm text-center">
|
| 415 |
+
<p className="text-[10px] font-bold text-gs-slate uppercase tracking-tighter">Current Plan</p>
|
| 416 |
+
<p className="text-lg font-semibold text-gs-navy">${(projectedTotal / 1000000).toFixed(2)}M</p>
|
| 417 |
+
</div>
|
| 418 |
+
<div className="p-4 bg-white border border-gray-100 rounded-xl shadow-sm text-center">
|
| 419 |
+
<p className="text-[10px] font-bold text-gs-slate uppercase tracking-tighter">Projected {gap > 0 ? 'Gap' : 'Surplus'}</p>
|
| 420 |
+
<p className={`text-lg font-semibold ${gap > 0 ? 'text-red-500' : 'text-green-600'}`}>
|
| 421 |
+
${Math.abs(gap / 1000000).toFixed(2)}M
|
| 422 |
+
</p>
|
| 423 |
+
</div>
|
| 424 |
+
<div className="p-4 bg-white border border-gray-100 rounded-xl shadow-sm text-center">
|
| 425 |
+
<p className="text-[10px] font-bold text-gs-slate uppercase tracking-tighter">Savings Rate</p>
|
| 426 |
+
<p className="text-lg font-semibold text-gs-gold">{contribution + employerMatch}%</p>
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
|
| 430 |
+
<div className="mt-4 p-4 bg-gs-navy text-white rounded-xl text-xs font-light">
|
| 431 |
+
<p><span className="text-gs-gold font-bold">PROJECTION BASIS:</span> Assumes {annualReturn}% annual return, inflation-adjusted spending, and adherence to the 4% safe withdrawal rule. Modeling includes GS institutional market assumptions.</p>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
);
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function TaxCalc({ inputs }) {
|
| 438 |
+
const { salary, contribution, initialSavings, annualReturn, age, retirementAge, taxBracket, futureTaxBracket } = inputs;
|
| 439 |
+
|
| 440 |
+
const years = retirementAge - age;
|
| 441 |
+
const rate = annualReturn / 100;
|
| 442 |
+
const annualContribution = salary * (contribution / 100);
|
| 443 |
+
|
| 444 |
+
// Traditional IRA: Invest pre-tax, pay tax at withdrawal
|
| 445 |
+
const tradFV_Initial = initialSavings * Math.pow(1 + rate, years);
|
| 446 |
+
const tradFV_Cont = annualContribution * (Math.pow(1 + rate, years) - 1) / rate;
|
| 447 |
+
const tradNet = (tradFV_Initial + tradFV_Cont) * (1 - (futureTaxBracket / 100));
|
| 448 |
+
|
| 449 |
+
// Roth IRA: Invest after-tax, tax-free withdrawal
|
| 450 |
+
const rothInitial = initialSavings * (1 - (taxBracket / 100));
|
| 451 |
+
const rothAnnual = annualContribution * (1 - (taxBracket / 100));
|
| 452 |
+
const rothFV_Initial = rothInitial * Math.pow(1 + rate, years);
|
| 453 |
+
const rothFV_Cont = rothAnnual * (Math.pow(1 + rate, years) - 1) / rate;
|
| 454 |
+
const rothNet = rothFV_Initial + rothFV_Cont;
|
| 455 |
+
|
| 456 |
+
const winner = rothNet > tradNet ? "Roth" : "Traditional";
|
| 457 |
+
const diffPercent = Math.abs(((rothNet - tradNet) / Math.min(rothNet, tradNet)) * 100).toFixed(1);
|
| 458 |
+
|
| 459 |
+
return (
|
| 460 |
+
<div className="space-y-8">
|
| 461 |
+
<div className="flex flex-col md:flex-row gap-4">
|
| 462 |
+
<div className={`flex-1 p-6 border rounded-2xl transition-all ${winner === 'Roth' ? 'border-purple-200 bg-purple-50/30' : 'border-gray-100 bg-gray-50/50 opacity-80'}`}>
|
| 463 |
+
<h4 className="text-purple-700 font-bold text-xs uppercase mb-4 flex justify-between">
|
| 464 |
+
Roth Strategy {winner === 'Roth' && <span className="bg-purple-100 text-[8px] px-2 py-0.5 rounded-full text-purple-700">OPTIMAL</span>}
|
| 465 |
+
</h4>
|
| 466 |
+
<p className="text-gs-navy text-sm font-light leading-relaxed">Pay taxes now at <span className="font-bold">{taxBracket}%</span>. All future growth and withdrawals are <span className="text-purple-700 font-bold underline">100% Tax-Free</span>.</p>
|
| 467 |
+
<div className="mt-6 pt-6 border-t border-purple-100">
|
| 468 |
+
<span className="text-3xl font-semibold text-purple-700">${(rothNet / 1000).toFixed(0)}k</span>
|
| 469 |
+
<p className="text-xs text-purple-600 mt-1 uppercase tracking-wider font-bold">Estimated Net Wealth</p>
|
| 470 |
+
</div>
|
| 471 |
+
</div>
|
| 472 |
+
|
| 473 |
+
<div className={`flex-1 p-6 border rounded-2xl transition-all ${winner === 'Traditional' ? 'border-gs-navy/20 bg-gs-light' : 'border-gray-100 bg-gray-50/50 opacity-80'}`}>
|
| 474 |
+
<h4 className="text-gs-navy font-bold text-xs uppercase mb-4 flex justify-between">
|
| 475 |
+
Traditional {winner === 'Traditional' && <span className="bg-gs-navy text-white text-[8px] px-2 py-0.5 rounded-full">OPTIMAL</span>}
|
| 476 |
+
</h4>
|
| 477 |
+
<p className="text-gs-navy text-sm font-light leading-relaxed">Get a tax deduction now. Entire balance is taxed at <span className="font-bold">{futureTaxBracket}%</span> during retirement.</p>
|
| 478 |
+
<div className="mt-6 pt-6 border-t border-gray-200">
|
| 479 |
+
<span className="text-3xl font-semibold text-gs-navy">${(tradNet / 1000).toFixed(0)}k</span>
|
| 480 |
+
<p className="text-xs text-gs-slate mt-1 uppercase tracking-wider font-bold">Estimated Net Wealth</p>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
</div>
|
| 484 |
+
|
| 485 |
+
<div className="bg-gs-navy text-white p-6 rounded-2xl flex flex-col md:flex-row justify-between items-center gap-4">
|
| 486 |
+
<div>
|
| 487 |
+
<h4 className="text-gs-gold font-bold text-[10px] uppercase tracking-widest mb-1">Strategic Selection</h4>
|
| 488 |
+
<p className="text-lg font-light">The <span className="font-bold text-gs-gold">{winner} Strategy</span> is projected to provide <span className="font-bold">{diffPercent}% more</span> spendable wealth.</p>
|
| 489 |
+
</div>
|
| 490 |
+
<div className="flex items-center gap-3">
|
| 491 |
+
<div className="text-right hidden md:block">
|
| 492 |
+
<p className="text-[10px] text-gray-400 font-bold uppercase">Difference</p>
|
| 493 |
+
<p className="text-gs-gold font-bold">${Math.abs((rothNet - tradNet) / 1000).toFixed(0)}k</p>
|
| 494 |
+
</div>
|
| 495 |
+
<div className="p-3 bg-gs-gold/20 rounded-full border border-gs-gold/30">
|
| 496 |
+
<Percent className="text-gs-gold" size={24} />
|
| 497 |
+
</div>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
);
|
| 502 |
+
}
|
| 503 |
+
|
src/components/Indicators.jsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Activity, ShieldAlert, Info, X, PieChart, DollarSign, Target } from 'lucide-react';
|
| 3 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 4 |
+
|
| 5 |
+
export default function Indicators({ portfolio }) {
|
| 6 |
+
const [activeInfo, setActiveInfo] = useState(null); // 'health' or 'risk'
|
| 7 |
+
|
| 8 |
+
// A simple gauge visualization using SVG
|
| 9 |
+
const dashArray = 283; // 2 * pi * r (r=45)
|
| 10 |
+
const dashOffset = dashArray - (dashArray * portfolio.healthScore) / 100;
|
| 11 |
+
|
| 12 |
+
return (
|
| 13 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 relative">
|
| 14 |
+
{/* Portfolio Health Score */}
|
| 15 |
+
<div
|
| 16 |
+
onClick={() => setActiveInfo('health')}
|
| 17 |
+
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex items-center justify-between cursor-pointer hover:border-gs-gold transition-all group"
|
| 18 |
+
>
|
| 19 |
+
<div>
|
| 20 |
+
<h3 className="text-sm text-gs-slate uppercase tracking-wider mb-1 font-medium flex items-center">
|
| 21 |
+
<Activity size={16} className="mr-2 text-gs-gold" /> Health Score
|
| 22 |
+
</h3>
|
| 23 |
+
<p className="text-3xl font-light text-gs-navy">
|
| 24 |
+
{portfolio.healthScore} <span className="text-base text-gray-400">/ 100</span>
|
| 25 |
+
</p>
|
| 26 |
+
<p className="text-sm text-gray-500 mt-2 font-light flex items-center group-hover:text-gs-gold transition-colors">
|
| 27 |
+
<Info size={14} className="mr-1" /> Click to see how this is calculated
|
| 28 |
+
</p>
|
| 29 |
+
</div>
|
| 30 |
+
<div className="relative w-24 h-24">
|
| 31 |
+
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
|
| 32 |
+
<circle cx="50" cy="50" r="45" fill="none" stroke="#f3f4f6" strokeWidth="8" />
|
| 33 |
+
<circle
|
| 34 |
+
cx="50" cy="50" r="45" fill="none"
|
| 35 |
+
stroke="#0B233F" strokeWidth="8"
|
| 36 |
+
strokeDasharray={dashArray} strokeDashoffset={dashOffset}
|
| 37 |
+
strokeLinecap="round"
|
| 38 |
+
className="transition-all duration-1000 ease-out"
|
| 39 |
+
/>
|
| 40 |
+
</svg>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
{/* Risk Meter */}
|
| 45 |
+
<div
|
| 46 |
+
onClick={() => setActiveInfo('risk')}
|
| 47 |
+
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex flex-col justify-center cursor-pointer hover:border-gs-gold transition-all group"
|
| 48 |
+
>
|
| 49 |
+
<h3 className="text-sm text-gs-slate uppercase tracking-wider mb-4 font-medium flex items-center">
|
| 50 |
+
<ShieldAlert size={16} className="mr-2 text-gs-gold" /> Risk Level
|
| 51 |
+
</h3>
|
| 52 |
+
<div className="flex w-full h-3 bg-gray-100 rounded-full overflow-hidden mb-3">
|
| 53 |
+
<div className={`h-full ${portfolio.riskLevel === 'Low' ? 'bg-gs-navy w-1/3' : portfolio.riskLevel === 'Medium' ? 'bg-gs-gold w-2/3' : 'bg-red-500 w-full'} transition-all duration-500`}></div>
|
| 54 |
+
</div>
|
| 55 |
+
<div className="flex justify-between text-xs text-gray-400 font-medium uppercase mb-2">
|
| 56 |
+
<span className={portfolio.riskLevel === 'Low' ? 'text-gs-navy font-bold' : ''}>Low</span>
|
| 57 |
+
<span className={portfolio.riskLevel === 'Medium' ? 'text-gs-gold font-bold' : ''}>Medium</span>
|
| 58 |
+
<span className={portfolio.riskLevel === 'High' ? 'text-red-500 font-bold' : ''}>High</span>
|
| 59 |
+
</div>
|
| 60 |
+
<p className="text-xs text-gray-400 font-light flex items-center group-hover:text-gs-gold transition-colors">
|
| 61 |
+
<Info size={12} className="mr-1" /> How we measure risk
|
| 62 |
+
</p>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
{/* Calculation Modal */}
|
| 66 |
+
<AnimatePresence>
|
| 67 |
+
{activeInfo && (
|
| 68 |
+
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4">
|
| 69 |
+
<motion.div
|
| 70 |
+
initial={{ opacity: 0 }}
|
| 71 |
+
animate={{ opacity: 1 }}
|
| 72 |
+
exit={{ opacity: 0 }}
|
| 73 |
+
onClick={() => setActiveInfo(null)}
|
| 74 |
+
className="absolute inset-0 bg-gs-navy/40 backdrop-blur-sm"
|
| 75 |
+
/>
|
| 76 |
+
<motion.div
|
| 77 |
+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
| 78 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 79 |
+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
| 80 |
+
className="relative bg-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden"
|
| 81 |
+
>
|
| 82 |
+
<div className="p-8">
|
| 83 |
+
<div className="flex justify-between items-start mb-6">
|
| 84 |
+
<div className="flex items-center">
|
| 85 |
+
<div className="p-3 bg-gs-light rounded-xl mr-4 text-gs-gold">
|
| 86 |
+
{activeInfo === 'health' ? <Activity size={24} /> : <ShieldAlert size={24} />}
|
| 87 |
+
</div>
|
| 88 |
+
<div>
|
| 89 |
+
<h4 className="text-xl font-semibold text-gs-navy">
|
| 90 |
+
{activeInfo === 'health' ? 'Health Score Logic' : 'Risk Calculation'}
|
| 91 |
+
</h4>
|
| 92 |
+
<p className="text-sm text-gs-slate font-light">Radical Transparency Report</p>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
<button onClick={() => setActiveInfo(null)} className="text-gray-400 hover:text-gs-navy transition-colors">
|
| 96 |
+
<X size={20} />
|
| 97 |
+
</button>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<div className="space-y-6">
|
| 101 |
+
{activeInfo === 'health' ? (
|
| 102 |
+
<>
|
| 103 |
+
<div className="flex gap-4">
|
| 104 |
+
<div className="mt-1 text-gs-gold"><DollarSign size={18} /></div>
|
| 105 |
+
<div>
|
| 106 |
+
<p className="font-medium text-gs-navy text-sm">Fee Efficiency</p>
|
| 107 |
+
<p className="text-xs text-gs-slate font-light leading-relaxed">We subtract points for high expense ratios. Every 0.1% in fees costs your portfolio health (Max -20 pts).</p>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
<div className="flex gap-4">
|
| 111 |
+
<div className="mt-1 text-gs-gold"><PieChart size={18} /></div>
|
| 112 |
+
<div>
|
| 113 |
+
<p className="font-medium text-gs-navy text-sm">Diversification Bonus</p>
|
| 114 |
+
<p className="text-xs text-gs-slate font-light leading-relaxed">Holding 5+ different asset classes grants a +5 point bonus for reduced concentration risk.</p>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
<div className="flex gap-4">
|
| 118 |
+
<div className="mt-1 text-gs-gold"><Target size={18} /></div>
|
| 119 |
+
<div>
|
| 120 |
+
<p className="font-medium text-gs-navy text-sm">Profile Alignment</p>
|
| 121 |
+
<p className="text-xs text-gs-slate font-light leading-relaxed">If your portfolio's actual volatility doesn't match your goal (e.g. Cautious vs Balanced), we subtract 15 points.</p>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</>
|
| 125 |
+
) : (
|
| 126 |
+
<div className="bg-gs-light p-5 rounded-2xl border border-gs-gold/20">
|
| 127 |
+
<p className="text-sm text-gs-navy font-medium mb-2">Market Sensitivity Logic</p>
|
| 128 |
+
<p className="text-xs text-gs-slate font-light leading-relaxed mb-4">
|
| 129 |
+
Risk isn't a guess. We measure how much your portfolio typically swings compared to the broader market to categorize your risk level.
|
| 130 |
+
</p>
|
| 131 |
+
|
| 132 |
+
<div className="bg-white/80 p-3 rounded-xl border border-gs-gold/10 mb-4 flex justify-between items-center shadow-sm">
|
| 133 |
+
<span className="text-[10px] uppercase tracking-widest text-gs-slate font-bold">Portfolio Beta</span>
|
| 134 |
+
<span className="text-xl font-bold text-gs-navy">{portfolio.avgBeta}</span>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<div className="space-y-2">
|
| 138 |
+
<div className="flex justify-between text-[10px] uppercase tracking-wider text-gs-slate font-bold">
|
| 139 |
+
<span>Low Risk</span>
|
| 140 |
+
<span className="text-gs-navy">Minimal Volatility</span>
|
| 141 |
+
</div>
|
| 142 |
+
<div className="flex justify-between text-[10px] uppercase tracking-wider text-gs-slate font-bold">
|
| 143 |
+
<span>Medium Risk</span>
|
| 144 |
+
<span className="text-gs-gold">Market Standard</span>
|
| 145 |
+
</div>
|
| 146 |
+
<div className="flex justify-between text-[10px] uppercase tracking-wider text-gs-slate font-bold">
|
| 147 |
+
<span>High Risk</span>
|
| 148 |
+
<span className="text-red-500">Aggressive Growth</span>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
)}
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<button
|
| 156 |
+
onClick={() => setActiveInfo(null)}
|
| 157 |
+
className="w-full mt-8 bg-gs-navy text-white py-4 rounded-xl font-medium hover:bg-gs-navy/90 transition-colors shadow-md"
|
| 158 |
+
>
|
| 159 |
+
Got it, thanks!
|
| 160 |
+
</button>
|
| 161 |
+
</div>
|
| 162 |
+
</motion.div>
|
| 163 |
+
</div>
|
| 164 |
+
)}
|
| 165 |
+
</AnimatePresence>
|
| 166 |
+
</div>
|
| 167 |
+
);
|
| 168 |
+
}
|
src/components/InvestmentCommittee.jsx
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
+
import { Users, User, BarChart2, AlertCircle, PlayCircle, Loader2 } from 'lucide-react';
|
| 4 |
+
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
|
| 5 |
+
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
| 6 |
+
|
| 7 |
+
const agents = {
|
| 8 |
+
macro: { name: 'Macro Economist', role: 'Analyzes broader economic trends, rates, and inflation', icon: <BarChart2 size={16} />, color: 'text-blue-500', bg: 'bg-blue-500/10' },
|
| 9 |
+
tech: { name: 'Technical Analyst', role: 'Focuses on momentum, moving averages, and charts', icon: <User size={16} />, color: 'text-purple-500', bg: 'bg-purple-500/10' },
|
| 10 |
+
skeptic: { name: 'The Skeptic', role: 'Looks for flaws, overvaluation, and risks', icon: <AlertCircle size={16} />, color: 'text-red-500', bg: 'bg-red-500/10' },
|
| 11 |
+
system: { name: 'System', role: 'Moderator', icon: <Users size={16} />, color: 'text-gs-gold', bg: 'bg-gs-gold/10' }
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
const mockDebates = {
|
| 15 |
+
'AAPL': [
|
| 16 |
+
{ agent: 'macro', text: 'iPhone demand remains steady globally, but services growth is the real story for Apple.' },
|
| 17 |
+
{ agent: 'tech', text: 'AAPL is consolidating near its 200-day average. A breakout above current levels would be very bullish.' },
|
| 18 |
+
{ agent: 'skeptic', text: 'Regulatory pressure in the EU and US is a massive dark cloud that could hurt margins.' },
|
| 19 |
+
{ agent: 'system', text: 'Consensus: Strong ecosystem but legal risks. Conviction Score: 72/100 (Hold).' }
|
| 20 |
+
],
|
| 21 |
+
'TSLA': [
|
| 22 |
+
{ agent: 'macro', text: 'High interest rates are cooling the EV market, forcing aggressive price cuts.' },
|
| 23 |
+
{ agent: 'tech', text: 'Tesla is in a clear downtrend. We need to see a higher low before turning bullish.' },
|
| 24 |
+
{ agent: 'skeptic', text: 'Elon is distracted and competition from China is fierce. Valuation is still disconnected from reality.' },
|
| 25 |
+
{ agent: 'system', text: 'Consensus: High volatility and macro headwinds. Conviction Score: 35/100 (Underweight).' }
|
| 26 |
+
],
|
| 27 |
+
'NVDA': [
|
| 28 |
+
{ agent: 'macro', text: 'The AI infrastructure build-out is a once-in-a-generation shift that favors NVIDIA.' },
|
| 29 |
+
{ agent: 'tech', text: 'Parabolic move. It is overextended, but momentum like this can last longer than expected.' },
|
| 30 |
+
{ agent: 'skeptic', text: 'At some point, the hyperscalers will stop buying at this rate. The drop will be as fast as the rise.' },
|
| 31 |
+
{ agent: 'system', text: 'Consensus: Unrivaled leader in a booming sector. Conviction Score: 88/100 (Overweight).' }
|
| 32 |
+
],
|
| 33 |
+
'MSFT': [
|
| 34 |
+
{ agent: 'macro', text: 'Enterprise software and cloud (Azure) are the backbone of the modern economy.' },
|
| 35 |
+
{ agent: 'tech', text: 'Steady uptrend. Microsoft is a "safe haven" in the tech world right now.' },
|
| 36 |
+
{ agent: 'skeptic', text: 'The Activision deal is done, but integrating it perfectly won\'t be easy or cheap.' },
|
| 37 |
+
{ agent: 'system', text: 'Consensus: Solid growth and AI tailwinds. Conviction Score: 82/100 (Overweight).' }
|
| 38 |
+
],
|
| 39 |
+
'default': [
|
| 40 |
+
{ agent: 'macro', text: 'The macro outlook for [TICKER] is heavily dependent on current sector trends and inflationary pressures affecting production costs.' },
|
| 41 |
+
{ agent: 'tech', text: '[TICKER] is currently testing a key resistance level. We need to see sustained volume before confirming a new upward trajectory.' },
|
| 42 |
+
{ agent: 'skeptic', text: 'A primary concern for [TICKER] is the potential for margin compression if competitors maintain aggressive pricing strategies.' },
|
| 43 |
+
{ agent: 'system', text: 'Consensus: Position is stable for [TICKER], but suggests a cautious stance until quarterly data confirms growth. Conviction Score: 62/100 (Neutral).' }
|
| 44 |
+
]
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
export default function InvestmentCommittee({ ticker, isDebating, setIsDebating }) {
|
| 48 |
+
const [messages, setMessages] = useState([]);
|
| 49 |
+
const [convictionScore, setConvictionScore] = useState(null);
|
| 50 |
+
const [loading, setLoading] = useState(false);
|
| 51 |
+
const scrollRef = useRef(null);
|
| 52 |
+
|
| 53 |
+
useEffect(() => {
|
| 54 |
+
setMessages([]);
|
| 55 |
+
setConvictionScore(null);
|
| 56 |
+
setIsDebating(false);
|
| 57 |
+
}, [ticker]);
|
| 58 |
+
|
| 59 |
+
useEffect(() => {
|
| 60 |
+
if (scrollRef.current) {
|
| 61 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 62 |
+
}
|
| 63 |
+
}, [messages]);
|
| 64 |
+
|
| 65 |
+
const runDebate = async () => {
|
| 66 |
+
if (!ticker) return;
|
| 67 |
+
setIsDebating(true);
|
| 68 |
+
setMessages([]);
|
| 69 |
+
setConvictionScore(null);
|
| 70 |
+
setLoading(true);
|
| 71 |
+
|
| 72 |
+
const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
|
| 73 |
+
|
| 74 |
+
if (apiKey && apiKey.length > 10) {
|
| 75 |
+
try {
|
| 76 |
+
const llm = new ChatGoogleGenerativeAI({
|
| 77 |
+
apiKey: apiKey,
|
| 78 |
+
modelName: 'gemini-1.5-flash',
|
| 79 |
+
maxOutputTokens: 2048,
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
// Agent 1: Macro
|
| 83 |
+
setMessages([{ agent: 'macro', text: `Quantifying macro factors for ${ticker}...` }]);
|
| 84 |
+
const macroResponse = await llm.invoke([
|
| 85 |
+
new SystemMessage(`You are a top-tier Macro Economist. Analyze the stock ${ticker} with extreme specificity. Mention a specific economic factor like "global logistics", "energy costs", or "labor markets" as it relates to this EXACT company. Provide a 12-month price target. 1 short sentence.`),
|
| 86 |
+
new HumanMessage(`What is your unique macro view on ${ticker}?`)
|
| 87 |
+
]);
|
| 88 |
+
setMessages([{ agent: 'macro', text: macroResponse.content }]);
|
| 89 |
+
|
| 90 |
+
// Agent 2: Tech
|
| 91 |
+
setMessages(prev => [...prev, { agent: 'tech', text: `Evaluating technical metrics for ${ticker}...` }]);
|
| 92 |
+
const techResponse = await llm.invoke([
|
| 93 |
+
new SystemMessage(`You are a Technical Analyst. Analyze ${ticker}'s price chart. Mention a specific "support zone", "RSI divergence", or "moving average cross" observation for this company. Provide a specific Fair Value. 1 sentence.`),
|
| 94 |
+
new HumanMessage(`Provide a non-generic technical view on ${ticker}.`)
|
| 95 |
+
]);
|
| 96 |
+
setMessages(prev => [prev[0], { agent: 'tech', text: techResponse.content }]);
|
| 97 |
+
|
| 98 |
+
// Agent 3: Skeptic
|
| 99 |
+
setMessages(prev => [...prev, { agent: 'skeptic', text: `Quantifying downside risks for ${ticker}...` }]);
|
| 100 |
+
const skepticResponse = await llm.invoke([
|
| 101 |
+
new SystemMessage(`You are a Professional Skeptic. Find a "poison pill" for ${ticker}. What is the one specific, non-obvious risk (e.g. patent cliff, specific litigation, supply chain bottleneck) for this stock? 1 sentence.`),
|
| 102 |
+
new HumanMessage(`What is the hidden risk in ${ticker}?`)
|
| 103 |
+
]);
|
| 104 |
+
setMessages(prev => [prev[0], prev[1], { agent: 'skeptic', text: skepticResponse.content }]);
|
| 105 |
+
|
| 106 |
+
// System consensus
|
| 107 |
+
setMessages(prev => [...prev, { agent: 'system', text: 'Synthesizing quantitative consensus...' }]);
|
| 108 |
+
const consensusResponse = await llm.invoke([
|
| 109 |
+
new SystemMessage("Committee Moderator. Based on the previous data points, output a final Conviction Score (0-100). Format: [SCORE] followed by a 1-sentence quantitative justification."),
|
| 110 |
+
new HumanMessage(`Synthesize these quantitative views for ${ticker}: 1. ${macroResponse.content} 2. ${techResponse.content} 3. ${skepticResponse.content}`)
|
| 111 |
+
]);
|
| 112 |
+
|
| 113 |
+
let score = parseInt(consensusResponse.content.replace(/\D/g,''));
|
| 114 |
+
if (isNaN(score)) score = 50;
|
| 115 |
+
|
| 116 |
+
setMessages(prev => [prev[0], prev[1], prev[2], { agent: 'system', text: consensusResponse.content }]);
|
| 117 |
+
setConvictionScore(score);
|
| 118 |
+
|
| 119 |
+
} catch (error) {
|
| 120 |
+
console.error("LLM Error:", error);
|
| 121 |
+
runMockDebate();
|
| 122 |
+
}
|
| 123 |
+
} else {
|
| 124 |
+
runMockDebate();
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
setLoading(false);
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
const runMockDebate = () => {
|
| 131 |
+
const script = mockDebates[ticker] || mockDebates['default'];
|
| 132 |
+
const debateScript = script.map(msg => ({
|
| 133 |
+
...msg,
|
| 134 |
+
text: msg.text.replace(/\[TICKER\]/g, ticker)
|
| 135 |
+
}));
|
| 136 |
+
|
| 137 |
+
let step = 0;
|
| 138 |
+
const interval = setInterval(() => {
|
| 139 |
+
if (step < debateScript.length) {
|
| 140 |
+
const msg = debateScript[step];
|
| 141 |
+
if (msg.agent === 'system' && msg.text.includes('Conviction Score')) {
|
| 142 |
+
const match = msg.text.match(/(\d+)\/100/);
|
| 143 |
+
if (match) setConvictionScore(parseInt(match[1]));
|
| 144 |
+
}
|
| 145 |
+
setMessages(prev => [...prev, msg]);
|
| 146 |
+
step++;
|
| 147 |
+
} else {
|
| 148 |
+
clearInterval(interval);
|
| 149 |
+
setLoading(false);
|
| 150 |
+
}
|
| 151 |
+
}, 1200);
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
return (
|
| 155 |
+
<div className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex flex-col h-[500px]">
|
| 156 |
+
<header className="mb-4 flex justify-between items-center border-b pb-4">
|
| 157 |
+
<div>
|
| 158 |
+
<h2 className="text-xl font-medium text-gs-navy flex items-center">
|
| 159 |
+
<Users className="mr-2 text-gs-gold" size={20} /> AI Investment Committee
|
| 160 |
+
</h2>
|
| 161 |
+
<p className="text-sm text-gs-slate font-light mt-1">
|
| 162 |
+
{ticker ? `Analyzing: ${ticker}` : 'Select an asset from your portfolio chart to debate.'}
|
| 163 |
+
</p>
|
| 164 |
+
</div>
|
| 165 |
+
{ticker && !isDebating && !convictionScore && (
|
| 166 |
+
<button
|
| 167 |
+
onClick={runDebate}
|
| 168 |
+
disabled={loading}
|
| 169 |
+
className="flex items-center px-4 py-2 bg-gs-navy text-white text-sm rounded-lg hover:bg-gs-navy/90 transition-colors"
|
| 170 |
+
>
|
| 171 |
+
{loading ? <Loader2 size={16} className="animate-spin mr-2" /> : <PlayCircle size={16} className="mr-2" />}
|
| 172 |
+
Start Debate
|
| 173 |
+
</button>
|
| 174 |
+
)}
|
| 175 |
+
</header>
|
| 176 |
+
|
| 177 |
+
<div className="flex-1 overflow-y-auto space-y-4 pr-2 custom-scrollbar" ref={scrollRef}>
|
| 178 |
+
{!ticker && (
|
| 179 |
+
<div className="h-full flex items-center justify-center text-gray-400 text-sm italic">
|
| 180 |
+
Waiting for asset selection...
|
| 181 |
+
</div>
|
| 182 |
+
)}
|
| 183 |
+
|
| 184 |
+
<AnimatePresence>
|
| 185 |
+
{messages.map((msg, idx) => (
|
| 186 |
+
<motion.div
|
| 187 |
+
key={idx}
|
| 188 |
+
initial={{ opacity: 0, y: 10 }}
|
| 189 |
+
animate={{ opacity: 1, y: 0 }}
|
| 190 |
+
className="flex gap-3"
|
| 191 |
+
>
|
| 192 |
+
<div className={`mt-1 flex-shrink-0 w-8 h-8 rounded-full ${agents[msg.agent].bg} ${agents[msg.agent].color} flex items-center justify-center`}>
|
| 193 |
+
{agents[msg.agent].icon}
|
| 194 |
+
</div>
|
| 195 |
+
<div className="bg-gray-50 rounded-xl p-3 text-sm text-gs-slate border border-gray-100 w-full">
|
| 196 |
+
<span className={`text-xs font-semibold uppercase tracking-wider block mb-1 ${agents[msg.agent].color}`}>
|
| 197 |
+
{agents[msg.agent].name}
|
| 198 |
+
</span>
|
| 199 |
+
{msg.text}
|
| 200 |
+
</div>
|
| 201 |
+
</motion.div>
|
| 202 |
+
))}
|
| 203 |
+
</AnimatePresence>
|
| 204 |
+
|
| 205 |
+
{loading && messages.length > 0 && messages.length < 4 && (
|
| 206 |
+
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex items-center text-gray-400 text-sm ml-11">
|
| 207 |
+
<Loader2 size={14} className="animate-spin mr-2" /> Typing...
|
| 208 |
+
</motion.div>
|
| 209 |
+
)}
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
{convictionScore !== null && (
|
| 213 |
+
<motion.div
|
| 214 |
+
initial={{ opacity: 0, scale: 0.95 }}
|
| 215 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 216 |
+
className="mt-4 pt-4 border-t"
|
| 217 |
+
>
|
| 218 |
+
<div className="bg-gs-light p-4 rounded-xl">
|
| 219 |
+
<div className="flex justify-between items-center">
|
| 220 |
+
<span className="font-medium text-gs-navy">Consensus Conviction Score</span>
|
| 221 |
+
<div className="flex items-center">
|
| 222 |
+
<span className={`text-2xl font-bold ${convictionScore >= 70 ? 'text-green-600' : convictionScore >= 40 ? 'text-gs-gold' : 'text-red-500'}`}>
|
| 223 |
+
{convictionScore}
|
| 224 |
+
</span>
|
| 225 |
+
<span className="text-gray-400 ml-1">/ 100</span>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
<p className="text-[10px] text-gs-slate mt-2 italic leading-tight border-t border-gray-200 pt-2">
|
| 229 |
+
The committee's confidence in this asset's current risk-to-reward.
|
| 230 |
+
<span className="font-bold ml-1">70+ Overweight</span>,
|
| 231 |
+
<span className="font-bold ml-1">40-69 Hold</span>,
|
| 232 |
+
<span className="font-bold ml-1">Below 40 Underweight</span>.
|
| 233 |
+
</p>
|
| 234 |
+
</div>
|
| 235 |
+
</motion.div>
|
| 236 |
+
)}
|
| 237 |
+
</div>
|
| 238 |
+
);
|
| 239 |
+
}
|
src/components/MacroTracker.jsx
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { Globe, Ship, AlertTriangle, ShieldCheck, Loader2, X } from 'lucide-react';
|
| 3 |
+
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
|
| 4 |
+
import { HumanMessage } from '@langchain/core/messages';
|
| 5 |
+
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
| 6 |
+
|
| 7 |
+
const newsHeadlines = [
|
| 8 |
+
{ source: 'Right-Leaning', headline: 'Supply Chain Bottlenecks Expose Dependence on Foreign Imports', sentiment: 'negative' },
|
| 9 |
+
{ source: 'Left-Leaning', headline: 'Global Trade Disruptions Threaten Consumer Price Stability', sentiment: 'negative' },
|
| 10 |
+
{ source: 'Financial Times', headline: 'Port Congestion Peaks as Holiday Inventory Arrives Early', sentiment: 'neutral' }
|
| 11 |
+
];
|
| 12 |
+
|
| 13 |
+
// Fallback data in case the ArcGIS API fails due to CORS or downtime
|
| 14 |
+
const mockPortData = Array.from({ length: 30 }, (_, i) => ({
|
| 15 |
+
date: new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
| 16 |
+
calls: Math.floor(12000 + Math.random() * 3000 + (i > 20 ? -2000 : 0)) // Simulating a recent dip
|
| 17 |
+
}));
|
| 18 |
+
|
| 19 |
+
export default function MacroTracker() {
|
| 20 |
+
const [isOpen, setIsOpen] = useState(false);
|
| 21 |
+
const [analysis, setAnalysis] = useState(null);
|
| 22 |
+
const [loading, setLoading] = useState(false);
|
| 23 |
+
const [chartData, setChartData] = useState([]);
|
| 24 |
+
const [isLive, setIsLive] = useState(false);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
const fetchPortWatchData = async () => {
|
| 28 |
+
try {
|
| 29 |
+
const apiUrl = "https://services9.arcgis.com/weJ1QsnbMYJlCHdG/arcgis/rest/services/Daily_Trade_Data/FeatureServer/0/query?where=1=1&outFields=date,calls&orderByFields=date DESC&resultRecordCount=30&f=json";
|
| 30 |
+
const response = await fetch(apiUrl);
|
| 31 |
+
if (!response.ok) throw new Error('Network response was not ok');
|
| 32 |
+
const data = await response.json();
|
| 33 |
+
|
| 34 |
+
if (data && data.features && data.features.length > 0) {
|
| 35 |
+
const parsedData = data.features.map(f => ({
|
| 36 |
+
date: new Date(f.attributes.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
| 37 |
+
calls: f.attributes.calls || 0
|
| 38 |
+
})).reverse();
|
| 39 |
+
setChartData(parsedData);
|
| 40 |
+
setIsLive(true);
|
| 41 |
+
} else {
|
| 42 |
+
throw new Error('No features returned');
|
| 43 |
+
}
|
| 44 |
+
} catch (error) {
|
| 45 |
+
console.warn("Failed to fetch live ArcGIS data. Falling back to mock dataset.", error);
|
| 46 |
+
setChartData(mockPortData);
|
| 47 |
+
setIsLive(false);
|
| 48 |
+
}
|
| 49 |
+
};
|
| 50 |
+
fetchPortWatchData();
|
| 51 |
+
}, []);
|
| 52 |
+
|
| 53 |
+
const synthesizeNews = async () => {
|
| 54 |
+
setLoading(true);
|
| 55 |
+
setAnalysis(null);
|
| 56 |
+
|
| 57 |
+
const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
|
| 58 |
+
|
| 59 |
+
if (apiKey && apiKey.length > 10) {
|
| 60 |
+
try {
|
| 61 |
+
const llm = new ChatGoogleGenerativeAI({
|
| 62 |
+
apiKey: apiKey,
|
| 63 |
+
modelName: 'gemini-1.5-flash',
|
| 64 |
+
maxOutputTokens: 2048,
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
const recentDataSummary = chartData.slice(-5).map(d => `${d.date}: ${d.calls} calls`).join(", ");
|
| 68 |
+
|
| 69 |
+
const promptText = `
|
| 70 |
+
You are an expert, calming financial advisor speaking to a novice investor.
|
| 71 |
+
The user is looking at a custom dashboard of global maritime trade data (port calls).
|
| 72 |
+
Here is the raw data for the last 5 days indicating recent activity levels: [${recentDataSummary}].
|
| 73 |
+
|
| 74 |
+
Additionally, read these recent news headlines causing panic:
|
| 75 |
+
1. ${newsHeadlines[0].headline}
|
| 76 |
+
2. ${newsHeadlines[1].headline}
|
| 77 |
+
3. ${newsHeadlines[2].headline}
|
| 78 |
+
|
| 79 |
+
Provide a grounded, jargon-free explanation (max 3-4 sentences) that synthesizes this numerical data and the news.
|
| 80 |
+
Explain why this data represents normal market noise and why they should not panic sell their portfolio.
|
| 81 |
+
`;
|
| 82 |
+
|
| 83 |
+
const message = new HumanMessage({
|
| 84 |
+
content: [{ type: "text", text: promptText }]
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
const response = await llm.invoke([message]);
|
| 88 |
+
setAnalysis(response.content);
|
| 89 |
+
setLoading(false);
|
| 90 |
+
} catch (error) {
|
| 91 |
+
console.error("Gemini Error:", error);
|
| 92 |
+
runMockAnalysis();
|
| 93 |
+
}
|
| 94 |
+
} else {
|
| 95 |
+
runMockAnalysis();
|
| 96 |
+
}
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
const runMockAnalysis = () => {
|
| 100 |
+
setTimeout(() => {
|
| 101 |
+
setAnalysis("While the news highlights supply chain bottlenecks, the port data shows that daily ship calls remain within normal seasonal ranges, despite a slight recent dip. This indicates that global trade is still flowing actively. Temporary disruptions and the resulting inflation spikes rarely derail a well-diversified, long-term portfolio. Stay the course and avoid making emotional decisions based on short-term headlines.");
|
| 102 |
+
setLoading(false);
|
| 103 |
+
}, 2000);
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
const handleOpen = () => {
|
| 107 |
+
setIsOpen(true);
|
| 108 |
+
if (!analysis) {
|
| 109 |
+
synthesizeNews();
|
| 110 |
+
}
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
return (
|
| 114 |
+
<>
|
| 115 |
+
<button
|
| 116 |
+
onClick={handleOpen}
|
| 117 |
+
className="w-full mt-8 py-4 bg-gs-navy text-white rounded-xl hover:bg-gs-navy/90 transition-all flex justify-center items-center font-medium shadow-md group"
|
| 118 |
+
>
|
| 119 |
+
<Globe className="mr-3 text-gs-gold group-hover:rotate-12 transition-transform" size={24} />
|
| 120 |
+
Generate Ground Truth (Global Uncertainty Tracker)
|
| 121 |
+
</button>
|
| 122 |
+
|
| 123 |
+
{isOpen && (
|
| 124 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
| 125 |
+
<div className="w-full max-w-5xl bg-white rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
| 126 |
+
<div className="bg-gs-navy p-6 text-white flex justify-between items-center sticky top-0 z-10">
|
| 127 |
+
<div>
|
| 128 |
+
<h2 className="text-xl font-medium flex items-center">
|
| 129 |
+
<Globe className="mr-2 text-gs-gold" size={20} /> Global Uncertainty Tracker
|
| 130 |
+
</h2>
|
| 131 |
+
<p className="text-sm text-gs-light/70 font-light mt-1">
|
| 132 |
+
Contextualizing geopolitical noise with real-world data to keep you grounded.
|
| 133 |
+
</p>
|
| 134 |
+
</div>
|
| 135 |
+
<button onClick={() => setIsOpen(false)} className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
| 136 |
+
<X size={24} />
|
| 137 |
+
</button>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div className="p-6 overflow-y-auto">
|
| 141 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
| 142 |
+
{/* Left: Custom Port Data Chart */}
|
| 143 |
+
<div className="flex flex-col">
|
| 144 |
+
<h3 className="text-sm text-gs-slate uppercase tracking-wider font-medium mb-3 flex items-center justify-between">
|
| 145 |
+
<div className="flex items-center">
|
| 146 |
+
<Ship size={16} className="mr-2 text-blue-500" /> Global Port Activity
|
| 147 |
+
</div>
|
| 148 |
+
<span className={`text-xs font-semibold px-2 py-1 rounded ${isLive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}`}>
|
| 149 |
+
{isLive ? 'LIVE IMF DATA' : 'MOCK DATA FALLBACK'}
|
| 150 |
+
</span>
|
| 151 |
+
</h3>
|
| 152 |
+
<div className="rounded-xl border border-gray-200 p-4 h-64 w-full bg-gray-50/50">
|
| 153 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 154 |
+
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
| 155 |
+
<defs>
|
| 156 |
+
<linearGradient id="colorCalls" x1="0" y1="0" x2="0" y2="1">
|
| 157 |
+
<stop offset="5%" stopColor="#1E293B" stopOpacity={0.8}/>
|
| 158 |
+
<stop offset="95%" stopColor="#1E293B" stopOpacity={0}/>
|
| 159 |
+
</linearGradient>
|
| 160 |
+
</defs>
|
| 161 |
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E7EB" />
|
| 162 |
+
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#64748B' }} minTickGap={30} />
|
| 163 |
+
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#64748B' }} />
|
| 164 |
+
<Tooltip
|
| 165 |
+
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
| 166 |
+
labelStyle={{ fontWeight: 'bold', color: '#1E293B' }}
|
| 167 |
+
/>
|
| 168 |
+
<Area type="monotone" dataKey="calls" stroke="#1E293B" strokeWidth={2} fillOpacity={1} fill="url(#colorCalls)" />
|
| 169 |
+
</AreaChart>
|
| 170 |
+
</ResponsiveContainer>
|
| 171 |
+
</div>
|
| 172 |
+
<p className="text-xs text-gs-slate mt-2 italic">Daily maritime trade volume index based on global port calls.</p>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
{/* Right: News */}
|
| 176 |
+
<div className="flex flex-col">
|
| 177 |
+
<h3 className="text-sm text-gs-slate uppercase tracking-wider font-medium mb-3 flex items-center">
|
| 178 |
+
<AlertTriangle size={16} className="mr-2 text-red-500" /> The News Cycle
|
| 179 |
+
</h3>
|
| 180 |
+
<div className="space-y-3 mb-6 flex-grow">
|
| 181 |
+
{newsHeadlines.map((news, idx) => (
|
| 182 |
+
<div key={idx} className="bg-gray-50 border-l-2 border-gs-gold p-3 rounded-r-lg text-sm">
|
| 183 |
+
<span className="text-xs font-semibold text-gs-slate mb-1 block uppercase tracking-wide">{news.source}</span>
|
| 184 |
+
<span className="text-gs-navy font-medium italic">"{news.headline}"</span>
|
| 185 |
+
</div>
|
| 186 |
+
))}
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
{/* Analysis Output */}
|
| 192 |
+
<div className="bg-gs-light p-6 rounded-xl border border-gray-200">
|
| 193 |
+
<div className="flex">
|
| 194 |
+
<div className="flex-shrink-0 mr-4">
|
| 195 |
+
<div className="w-10 h-10 rounded-full bg-gs-navy flex items-center justify-center text-gs-gold">
|
| 196 |
+
{loading ? <Loader2 size={20} className="animate-spin" /> : <ShieldCheck size={20} />}
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
<div>
|
| 200 |
+
<h4 className="text-sm font-semibold uppercase tracking-wider text-gs-navy mb-1">
|
| 201 |
+
{loading ? 'Synthesizing Ground Truth...' : 'Grounded Analysis'}
|
| 202 |
+
</h4>
|
| 203 |
+
{analysis ? (
|
| 204 |
+
<p className="text-gs-slate leading-relaxed font-light">{analysis}</p>
|
| 205 |
+
) : (
|
| 206 |
+
<div className="h-4 bg-gray-200 rounded w-3/4 animate-pulse mt-2"></div>
|
| 207 |
+
)}
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
)}
|
| 215 |
+
</>
|
| 216 |
+
);
|
| 217 |
+
}
|
src/components/PortfolioHeatmap.jsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo } from 'react';
|
| 2 |
+
import { Treemap, ResponsiveContainer, Tooltip } from 'recharts';
|
| 3 |
+
import { getHistoricalData } from '../data/historicalData';
|
| 4 |
+
import { motion } from 'framer-motion';
|
| 5 |
+
import { X, Info } from 'lucide-react';
|
| 6 |
+
|
| 7 |
+
const HeatmapContent = (props) => {
|
| 8 |
+
const { x, y, width, height, name, performance } = props;
|
| 9 |
+
|
| 10 |
+
if (width < 5 || height < 5) return null;
|
| 11 |
+
|
| 12 |
+
const color = performance > 0
|
| 13 |
+
? `rgba(0, 200, 5, ${Math.min(0.2 + Math.abs(performance) / 5, 0.9)})`
|
| 14 |
+
: `rgba(255, 80, 0, ${Math.min(0.2 + Math.abs(performance) / 5, 0.9)})`;
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<g>
|
| 18 |
+
<rect
|
| 19 |
+
x={x}
|
| 20 |
+
y={y}
|
| 21 |
+
width={width}
|
| 22 |
+
height={height}
|
| 23 |
+
style={{
|
| 24 |
+
fill: color,
|
| 25 |
+
stroke: '#fff',
|
| 26 |
+
strokeWidth: 1,
|
| 27 |
+
strokeOpacity: 0.2,
|
| 28 |
+
}}
|
| 29 |
+
/>
|
| 30 |
+
{width > 40 && height > 30 && (
|
| 31 |
+
<text
|
| 32 |
+
x={x + width / 2}
|
| 33 |
+
y={y + height / 2 + 5}
|
| 34 |
+
textAnchor="middle"
|
| 35 |
+
fill="#fff"
|
| 36 |
+
fontSize={Math.min(width / 6, 12)}
|
| 37 |
+
fontWeight="bold"
|
| 38 |
+
className="pointer-events-none select-none"
|
| 39 |
+
>
|
| 40 |
+
{name}
|
| 41 |
+
</text>
|
| 42 |
+
)}
|
| 43 |
+
</g>
|
| 44 |
+
);
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
export default function PortfolioHeatmap({ onClose, allocation }) {
|
| 48 |
+
const heatmapData = useMemo(() => {
|
| 49 |
+
if (!allocation) return [];
|
| 50 |
+
return allocation.map(asset => {
|
| 51 |
+
try {
|
| 52 |
+
const hist = getHistoricalData(asset.ticker);
|
| 53 |
+
return {
|
| 54 |
+
name: asset.ticker,
|
| 55 |
+
size: asset.value || 1,
|
| 56 |
+
performance: hist?.percentChange || 0,
|
| 57 |
+
fullName: asset.name
|
| 58 |
+
};
|
| 59 |
+
} catch (e) {
|
| 60 |
+
return {
|
| 61 |
+
name: asset.ticker,
|
| 62 |
+
size: asset.value || 1,
|
| 63 |
+
performance: 0,
|
| 64 |
+
fullName: asset.name
|
| 65 |
+
};
|
| 66 |
+
}
|
| 67 |
+
});
|
| 68 |
+
}, [allocation]);
|
| 69 |
+
|
| 70 |
+
return (
|
| 71 |
+
<div className="fixed inset-0 z-[70] flex items-center justify-center p-4">
|
| 72 |
+
<motion.div
|
| 73 |
+
initial={{ opacity: 0 }}
|
| 74 |
+
animate={{ opacity: 1 }}
|
| 75 |
+
exit={{ opacity: 0 }}
|
| 76 |
+
onClick={onClose}
|
| 77 |
+
className="absolute inset-0 bg-gs-navy/60 backdrop-blur-md"
|
| 78 |
+
/>
|
| 79 |
+
<motion.div
|
| 80 |
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 81 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 82 |
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 83 |
+
className="relative bg-[#111111] rounded-3xl shadow-2xl w-full max-w-5xl overflow-hidden border border-white/10 flex flex-col max-h-[90vh]"
|
| 84 |
+
>
|
| 85 |
+
{/* Header */}
|
| 86 |
+
<div className="p-6 md:p-8 border-b border-white/5 flex justify-between items-center bg-black/20">
|
| 87 |
+
<div>
|
| 88 |
+
<h2 className="text-2xl font-bold text-white flex items-center">
|
| 89 |
+
Portfolio Heatmap
|
| 90 |
+
<span className="ml-3 px-2 py-1 bg-white/10 rounded text-[10px] uppercase tracking-widest text-white/60 font-medium">
|
| 91 |
+
Live Analysis
|
| 92 |
+
</span>
|
| 93 |
+
</h2>
|
| 94 |
+
<p className="text-gray-400 text-sm mt-1 font-light">
|
| 95 |
+
Box size: Allocation % | Color: Daily Performance
|
| 96 |
+
</p>
|
| 97 |
+
</div>
|
| 98 |
+
<button
|
| 99 |
+
onClick={onClose}
|
| 100 |
+
className="p-2 rounded-full bg-white/5 text-white/40 hover:text-white hover:bg-white/10 transition-colors"
|
| 101 |
+
>
|
| 102 |
+
<X size={24} />
|
| 103 |
+
</button>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div className="p-6 md:p-8 overflow-y-auto">
|
| 107 |
+
<div className="h-[400px] md:h-[500px] w-full bg-black/40 rounded-2xl overflow-hidden border border-white/5">
|
| 108 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 109 |
+
<Treemap
|
| 110 |
+
data={heatmapData}
|
| 111 |
+
dataKey="size"
|
| 112 |
+
aspectRatio={4 / 3}
|
| 113 |
+
stroke="#fff"
|
| 114 |
+
content={<HeatmapContent />}
|
| 115 |
+
>
|
| 116 |
+
<Tooltip
|
| 117 |
+
content={({ active, payload }) => {
|
| 118 |
+
if (active && payload && payload.length) {
|
| 119 |
+
const data = payload[0].payload;
|
| 120 |
+
return (
|
| 121 |
+
<div className="bg-[#222] border border-white/10 p-4 rounded-xl shadow-2xl min-w-[180px]">
|
| 122 |
+
<p className="text-white font-bold text-base">{data.name}</p>
|
| 123 |
+
<p className="text-gray-400 text-[10px] mb-3 truncate max-w-[160px]">{data.fullName}</p>
|
| 124 |
+
<div className="flex justify-between items-center border-t border-white/5 pt-2">
|
| 125 |
+
<div className="text-left">
|
| 126 |
+
<span className="text-[9px] text-gray-500 uppercase block">Allocation</span>
|
| 127 |
+
<span className="text-white text-sm font-medium">{data.size}%</span>
|
| 128 |
+
</div>
|
| 129 |
+
<div className="text-right">
|
| 130 |
+
<span className="text-[9px] text-gray-500 uppercase block">Day Change</span>
|
| 131 |
+
<span className={`text-sm font-bold ${data.performance > 0 ? 'text-[#00C805]' : 'text-[#FF5000]'}`}>
|
| 132 |
+
{data.performance > 0 ? '+' : ''}{data.performance}%
|
| 133 |
+
</span>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
);
|
| 138 |
+
}
|
| 139 |
+
return null;
|
| 140 |
+
}}
|
| 141 |
+
/>
|
| 142 |
+
</Treemap>
|
| 143 |
+
</ResponsiveContainer>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
{/* Legend */}
|
| 147 |
+
<div className="mt-8 flex flex-wrap items-center justify-between gap-6">
|
| 148 |
+
<div className="flex items-center gap-6">
|
| 149 |
+
<div className="flex items-center gap-2">
|
| 150 |
+
<div className="w-3 h-3 bg-[#FF5000] rounded-sm shadow-[0_0_10px_rgba(255,80,0,0.3)]"></div>
|
| 151 |
+
<span className="text-[11px] text-gray-400 font-medium">Down</span>
|
| 152 |
+
</div>
|
| 153 |
+
<div className="flex items-center gap-2">
|
| 154 |
+
<div className="w-3 h-3 bg-[#00C805] rounded-sm shadow-[0_0_10px_rgba(0,200,5,0.3)]"></div>
|
| 155 |
+
<span className="text-[11px] text-gray-400 font-medium">Up</span>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<div className="flex items-center gap-2 text-gray-500 bg-white/5 px-4 py-2 rounded-lg border border-white/5">
|
| 160 |
+
<Info size={14} className="text-gs-gold" />
|
| 161 |
+
<span className="text-[10px] uppercase tracking-wider font-medium">Color intensity scales with volatility</span>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
</motion.div>
|
| 166 |
+
</div>
|
| 167 |
+
);
|
| 168 |
+
}
|
src/components/RebalancingEngine.jsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { rebalancingScenarios } from '../data/mockData';
|
| 3 |
+
import { TrendingDown, ArrowUpRight, HandCoins } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
export default function RebalancingEngine({ onScenarioSelect }) {
|
| 6 |
+
const scenarios = [
|
| 7 |
+
{ id: 'marketDrop', icon: <TrendingDown size={20} />, data: rebalancingScenarios.marketDrop },
|
| 8 |
+
{ id: 'inflation', icon: <ArrowUpRight size={20} />, data: rebalancingScenarios.inflation },
|
| 9 |
+
{ id: 'withdrawal', icon: <HandCoins size={20} />, data: rebalancingScenarios.withdrawal },
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
return (
|
| 13 |
+
<div className="bg-white rounded-2xl p-8 shadow-sm border border-gray-100 h-full">
|
| 14 |
+
<header className="mb-8">
|
| 15 |
+
<h2 className="text-2xl font-light text-gs-navy mb-2">Scenario Planner</h2>
|
| 16 |
+
<p className="text-sm text-gs-slate font-light">
|
| 17 |
+
Life happens. See how your portfolio should adapt to different situations.
|
| 18 |
+
</p>
|
| 19 |
+
</header>
|
| 20 |
+
|
| 21 |
+
<div className="space-y-4">
|
| 22 |
+
{scenarios.map((scenario) => (
|
| 23 |
+
<button
|
| 24 |
+
key={scenario.id}
|
| 25 |
+
onClick={() => onScenarioSelect(scenario.data)}
|
| 26 |
+
className="w-full text-left p-5 rounded-xl border border-gray-200 hover:border-gs-gold hover:shadow-md transition-all duration-300 group bg-gs-light/50 hover:bg-white"
|
| 27 |
+
>
|
| 28 |
+
<div className="flex items-center mb-3">
|
| 29 |
+
<div className="w-10 h-10 rounded-full bg-white shadow-sm flex items-center justify-center mr-4 text-gs-slate group-hover:text-gs-gold transition-colors">
|
| 30 |
+
{scenario.icon}
|
| 31 |
+
</div>
|
| 32 |
+
<h3 className="font-medium text-gs-navy">{scenario.data.trigger}</h3>
|
| 33 |
+
</div>
|
| 34 |
+
<p className="text-sm text-gs-slate font-light ml-14 group-hover:text-gs-navy transition-colors">
|
| 35 |
+
Click to see recommended actions
|
| 36 |
+
</p>
|
| 37 |
+
</button>
|
| 38 |
+
))}
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
);
|
| 42 |
+
}
|
src/components/StockPopup.jsx
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState, useMemo } from 'react';
|
| 2 |
+
import { X, TrendingUp, TrendingDown, Star } from 'lucide-react';
|
| 3 |
+
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
|
| 4 |
+
import { getHistoricalData, fetchRealData } from '../data/historicalData';
|
| 5 |
+
import { Loader2 } from 'lucide-react';
|
| 6 |
+
import InvestmentCommittee from './InvestmentCommittee';
|
| 7 |
+
|
| 8 |
+
const TIMEFRAME_DAYS = {
|
| 9 |
+
'1W': 7,
|
| 10 |
+
'1M': 30,
|
| 11 |
+
'3M': 90,
|
| 12 |
+
'1Y': 365,
|
| 13 |
+
'ALL': 365
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
export default function StockPopup({ ticker, assetName, onClose }) {
|
| 17 |
+
const [data, setData] = useState(null);
|
| 18 |
+
const [loading, setLoading] = useState(false);
|
| 19 |
+
const [isDebating, setIsDebating] = useState(false);
|
| 20 |
+
const [timeframe, setTimeframe] = useState('1M');
|
| 21 |
+
|
| 22 |
+
useEffect(() => {
|
| 23 |
+
async function loadData() {
|
| 24 |
+
if (ticker) {
|
| 25 |
+
setLoading(true);
|
| 26 |
+
const realData = await fetchRealData(ticker);
|
| 27 |
+
setData(realData);
|
| 28 |
+
setLoading(false);
|
| 29 |
+
setIsDebating(false);
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
loadData();
|
| 33 |
+
}, [ticker]);
|
| 34 |
+
|
| 35 |
+
const viewData = useMemo(() => {
|
| 36 |
+
if (!data) return null;
|
| 37 |
+
|
| 38 |
+
// Slice history based on timeframe
|
| 39 |
+
const daysToShow = TIMEFRAME_DAYS[timeframe] || 30;
|
| 40 |
+
const startIndex = Math.max(0, data.history.length - daysToShow);
|
| 41 |
+
const slicedHistory = data.history.slice(startIndex);
|
| 42 |
+
|
| 43 |
+
// Recalculate metrics for the specific timeframe
|
| 44 |
+
if (slicedHistory.length === 0) return null;
|
| 45 |
+
|
| 46 |
+
const currentPrice = slicedHistory[slicedHistory.length - 1].price;
|
| 47 |
+
const oldPrice = slicedHistory[0].price;
|
| 48 |
+
const change = Number((currentPrice - oldPrice).toFixed(2));
|
| 49 |
+
const percentChange = Number(((change / oldPrice) * 100).toFixed(2));
|
| 50 |
+
const isPositive = change >= 0;
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
...data,
|
| 54 |
+
history: slicedHistory,
|
| 55 |
+
change,
|
| 56 |
+
percentChange,
|
| 57 |
+
isPositive
|
| 58 |
+
};
|
| 59 |
+
}, [data, timeframe]);
|
| 60 |
+
|
| 61 |
+
if (!ticker) return null;
|
| 62 |
+
|
| 63 |
+
if (loading || !viewData) {
|
| 64 |
+
return (
|
| 65 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
| 66 |
+
<div className="text-center">
|
| 67 |
+
<Loader2 className="animate-spin text-gs-gold mb-4 mx-auto" size={48} />
|
| 68 |
+
<p className="text-white text-lg font-light">Connecting to Yahoo Finance...</p>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
const color = viewData.isPositive ? '#00C805' : '#FF5000';
|
| 75 |
+
const bgColor = '#111111'; // Dark theme background
|
| 76 |
+
|
| 77 |
+
return (
|
| 78 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
| 79 |
+
<div
|
| 80 |
+
className="w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-xl shadow-2xl flex flex-col animate-in zoom-in-95 duration-200"
|
| 81 |
+
style={{ backgroundColor: bgColor }}
|
| 82 |
+
>
|
| 83 |
+
{/* Header */}
|
| 84 |
+
<div className="p-6 pb-2 flex justify-between items-start sticky top-0 bg-[#111111] z-10 border-b border-gray-800">
|
| 85 |
+
<div>
|
| 86 |
+
<h2 className="text-2xl font-bold text-white flex items-center">
|
| 87 |
+
{assetName} <Star size={18} className="ml-3 text-gray-500 hover:text-yellow-400 cursor-pointer" />
|
| 88 |
+
</h2>
|
| 89 |
+
<div className="flex items-end mt-2 space-x-3">
|
| 90 |
+
<span className="text-4xl font-bold text-white">${viewData.currentPrice}</span>
|
| 91 |
+
<div className={`flex items-center text-lg font-medium pb-1 ${viewData.isPositive ? 'text-[#00C805]' : 'text-[#FF5000]'}`}>
|
| 92 |
+
{viewData.isPositive ? '+' : ''}{viewData.change} ({viewData.isPositive ? '+' : ''}{viewData.percentChange}%)
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
<p className="text-gray-400 text-xs mt-1">At close: {viewData.history[viewData.history.length-1].date}</p>
|
| 96 |
+
</div>
|
| 97 |
+
<button
|
| 98 |
+
onClick={onClose}
|
| 99 |
+
className="p-2 rounded-full bg-gray-800 text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
|
| 100 |
+
>
|
| 101 |
+
<X size={20} />
|
| 102 |
+
</button>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<div className="p-6 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 106 |
+
{/* Main Chart Area */}
|
| 107 |
+
<div className="lg:col-span-2">
|
| 108 |
+
<div className="h-72 w-full mt-4">
|
| 109 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 110 |
+
<AreaChart data={viewData.history} margin={{ top: 10, right: 0, left: 0, bottom: 0 }}>
|
| 111 |
+
<defs>
|
| 112 |
+
<linearGradient id="colorPrice" x1="0" y1="0" x2="0" y2="1">
|
| 113 |
+
<stop offset="5%" stopColor={color} stopOpacity={0.3}/>
|
| 114 |
+
<stop offset="95%" stopColor={color} stopOpacity={0}/>
|
| 115 |
+
</linearGradient>
|
| 116 |
+
</defs>
|
| 117 |
+
<XAxis dataKey="date" hide />
|
| 118 |
+
<YAxis domain={['dataMin', 'dataMax']} hide />
|
| 119 |
+
<Tooltip
|
| 120 |
+
contentStyle={{ backgroundColor: '#222', border: 'none', borderRadius: '8px', color: '#fff' }}
|
| 121 |
+
itemStyle={{ color: '#fff', fontWeight: 'bold' }}
|
| 122 |
+
labelStyle={{ color: '#888' }}
|
| 123 |
+
/>
|
| 124 |
+
<ReferenceLine y={viewData.history[0].price} stroke="#333" strokeDasharray="3 3" />
|
| 125 |
+
<Area
|
| 126 |
+
type="monotone"
|
| 127 |
+
dataKey="price"
|
| 128 |
+
stroke={color}
|
| 129 |
+
strokeWidth={2}
|
| 130 |
+
fillOpacity={1}
|
| 131 |
+
fill="url(#colorPrice)"
|
| 132 |
+
/>
|
| 133 |
+
</AreaChart>
|
| 134 |
+
</ResponsiveContainer>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
{/* Time Controls */}
|
| 138 |
+
<div className="flex space-x-4 mt-6 border-b border-gray-800 pb-2">
|
| 139 |
+
{['1W', '1M', '3M', '1Y', 'ALL'].map(t => (
|
| 140 |
+
<button
|
| 141 |
+
key={t}
|
| 142 |
+
onClick={() => setTimeframe(t)}
|
| 143 |
+
className={`text-sm font-medium pb-2 border-b-2 transition-colors ${
|
| 144 |
+
t === timeframe
|
| 145 |
+
? (viewData.isPositive ? 'text-[#00C805] border-[#00C805]' : 'text-[#FF5000] border-[#FF5000]')
|
| 146 |
+
: 'text-gray-500 border-transparent hover:text-gray-300'
|
| 147 |
+
}`}
|
| 148 |
+
>
|
| 149 |
+
{t}
|
| 150 |
+
</button>
|
| 151 |
+
))}
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<div className="mt-6 text-gray-300 text-sm leading-relaxed">
|
| 155 |
+
<p>Historical charting for {assetName} over the selected period. Notice the volatility patterns represented by the area chart.</p>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
{/* Right Sidebar: AI Committee */}
|
| 160 |
+
<div className="lg:col-span-1">
|
| 161 |
+
{/* We render the Investment Committee inside a styled container so it fits the dark theme or stands out as a module */}
|
| 162 |
+
<div className="bg-white rounded-xl shadow-inner overflow-hidden border border-gray-200">
|
| 163 |
+
<InvestmentCommittee
|
| 164 |
+
ticker={ticker}
|
| 165 |
+
isDebating={isDebating}
|
| 166 |
+
setIsDebating={setIsDebating}
|
| 167 |
+
/>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
);
|
| 174 |
+
}
|
src/components/StockSearch.jsx
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { Search, Plus, Loader2, X, Globe, Briefcase, Info, TrendingUp, ShieldCheck, PlayCircle } from 'lucide-react';
|
| 3 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 4 |
+
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
|
| 5 |
+
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
| 6 |
+
|
| 7 |
+
export default function StockSearch({ isOpen, onClose, onAddStock, currentPortfolio, totalValue }) {
|
| 8 |
+
const [query, setQuery] = useState('');
|
| 9 |
+
const [suggestions, setSuggestions] = useState([]);
|
| 10 |
+
const [loading, setLoading] = useState(false);
|
| 11 |
+
const [selectedStock, setSelectedStock] = useState(null);
|
| 12 |
+
const [shares, setShares] = useState(1);
|
| 13 |
+
const [impactAnalysis, setImpactAnalysis] = useState('');
|
| 14 |
+
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
| 15 |
+
const [performanceData, setPerformanceData] = useState({ threeYearReturn: 0 });
|
| 16 |
+
const [error, setError] = useState('');
|
| 17 |
+
const searchRef = useRef(null);
|
| 18 |
+
|
| 19 |
+
// Debounced search for suggestions
|
| 20 |
+
useEffect(() => {
|
| 21 |
+
const timer = setTimeout(async () => {
|
| 22 |
+
if (query.length > 1) {
|
| 23 |
+
setLoading(true);
|
| 24 |
+
try {
|
| 25 |
+
const proxy = 'https://corsproxy.io/?';
|
| 26 |
+
const url = `${proxy}https://query2.finance.yahoo.com/v1/finance/search?q=${query}&newsCount=0`;
|
| 27 |
+
const response = await fetch(url);
|
| 28 |
+
const data = await response.json();
|
| 29 |
+
|
| 30 |
+
if (data.quotes) {
|
| 31 |
+
// Filter for US Stocks and Mutual Funds only
|
| 32 |
+
const usExchanges = ['NYSE', 'NASDAQ', 'AMEX', 'BATS', 'ARCA', 'OTCQB', 'OTCQX'];
|
| 33 |
+
const filtered = data.quotes
|
| 34 |
+
.filter(q => {
|
| 35 |
+
const isUS = usExchanges.some(ex => q.exchDisp?.includes(ex)) || !q.symbol.includes('.');
|
| 36 |
+
const isTargetType = q.quoteType === 'EQUITY' || q.quoteType === 'MUTUALFUND';
|
| 37 |
+
return q.symbol && isUS && isTargetType;
|
| 38 |
+
})
|
| 39 |
+
.slice(0, 6);
|
| 40 |
+
setSuggestions(filtered);
|
| 41 |
+
}
|
| 42 |
+
} catch (err) {
|
| 43 |
+
console.error("Autocomplete error:", err);
|
| 44 |
+
} finally {
|
| 45 |
+
setLoading(false);
|
| 46 |
+
}
|
| 47 |
+
} else {
|
| 48 |
+
setSuggestions([]);
|
| 49 |
+
}
|
| 50 |
+
}, 300);
|
| 51 |
+
|
| 52 |
+
return () => clearTimeout(timer);
|
| 53 |
+
}, [query]);
|
| 54 |
+
|
| 55 |
+
// Reset states when selectedStock is cleared
|
| 56 |
+
useEffect(() => {
|
| 57 |
+
if (!selectedStock) {
|
| 58 |
+
setImpactAnalysis('');
|
| 59 |
+
setShares(1);
|
| 60 |
+
}
|
| 61 |
+
}, [selectedStock]);
|
| 62 |
+
|
| 63 |
+
// Calculate 3-year performance whenever selection changes
|
| 64 |
+
useEffect(() => {
|
| 65 |
+
if (selectedStock) {
|
| 66 |
+
// Generate a stable mock performance based on ticker hash
|
| 67 |
+
const ticker = selectedStock.symbol;
|
| 68 |
+
let hash = 0;
|
| 69 |
+
for (let i = 0; i < ticker.length; i++) {
|
| 70 |
+
hash = ((hash << 5) - hash) + ticker.charCodeAt(i);
|
| 71 |
+
hash |= 0;
|
| 72 |
+
}
|
| 73 |
+
// Generate return between -20% and +120%
|
| 74 |
+
const returnVal = (hash % 140) - 20;
|
| 75 |
+
setPerformanceData({ threeYearReturn: returnVal });
|
| 76 |
+
}
|
| 77 |
+
}, [selectedStock]);
|
| 78 |
+
|
| 79 |
+
const analyzeImpact = async (stock, quantity) => {
|
| 80 |
+
console.log("Starting analyzeImpact", { stock: stock.symbol, quantity });
|
| 81 |
+
const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
|
| 82 |
+
|
| 83 |
+
if (!apiKey || apiKey.length < 10) {
|
| 84 |
+
console.warn("API Key missing or too short, using local fallback");
|
| 85 |
+
setImpactAnalysis(`Adding ${quantity} shares of ${stock.symbol} will broaden your market exposure and shift your portfolio beta towards a more dynamic profile.`);
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
setIsAnalyzing(true);
|
| 90 |
+
setImpactAnalysis(''); // Clear previous
|
| 91 |
+
|
| 92 |
+
try {
|
| 93 |
+
console.log("Invoking Gemini for impact analysis...");
|
| 94 |
+
const llm = new ChatGoogleGenerativeAI({
|
| 95 |
+
apiKey,
|
| 96 |
+
modelName: 'gemini-1.5-flash',
|
| 97 |
+
maxRetries: 1
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
const portfolioTickers = currentPortfolio.allocation.map(a => a.ticker).join(', ') || 'none';
|
| 101 |
+
|
| 102 |
+
const response = await llm.invoke([
|
| 103 |
+
new SystemMessage(`You are a quantitative advisor. Analyze the impact of adding ${quantity} shares of ${stock.symbol} to a portfolio of ${portfolioTickers}.
|
| 104 |
+
Output 1 short, specific sentence.`),
|
| 105 |
+
new HumanMessage(`Impact of ${quantity} shares of ${stock.symbol}.`)
|
| 106 |
+
]);
|
| 107 |
+
|
| 108 |
+
console.log("AI Response received:", response.content);
|
| 109 |
+
setImpactAnalysis(response.content);
|
| 110 |
+
} catch (err) {
|
| 111 |
+
console.error("Impact Analysis API Error:", err);
|
| 112 |
+
// Immediate fallback so the user doesn't see a blank screen
|
| 113 |
+
const fallbackText = `This position in ${stock.symbol} provides new exposure that complements your existing holdings in ${currentPortfolio.allocation[0]?.ticker || 'diversified assets'}.`;
|
| 114 |
+
setImpactAnalysis(fallbackText);
|
| 115 |
+
} finally {
|
| 116 |
+
setIsAnalyzing(false);
|
| 117 |
+
console.log("Analysis cycle complete");
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
const handleSelect = (quote) => {
|
| 122 |
+
setSelectedStock(quote);
|
| 123 |
+
// Initial analysis triggered by useEffect
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
const handleConfirm = () => {
|
| 127 |
+
onAddStock({
|
| 128 |
+
ticker: selectedStock.symbol,
|
| 129 |
+
name: selectedStock.shortname || selectedStock.longname || selectedStock.symbol,
|
| 130 |
+
type: selectedStock.quoteType === 'MUTUALFUND' ? 'mf' : 'stock',
|
| 131 |
+
value: 5, // Base weighting, dashboard handles rebalance
|
| 132 |
+
color: `#${Math.floor(Math.random()*16777215).toString(16)}`,
|
| 133 |
+
shares: Number(shares),
|
| 134 |
+
dateBought: new Date().toISOString().split('T')[0]
|
| 135 |
+
});
|
| 136 |
+
setQuery('');
|
| 137 |
+
setSelectedStock(null);
|
| 138 |
+
setSuggestions([]);
|
| 139 |
+
onClose();
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
if (!isOpen) return null;
|
| 143 |
+
|
| 144 |
+
return (
|
| 145 |
+
<div className="fixed inset-0 z-[60] flex items-start justify-center pt-24 px-4 bg-gs-navy/40 backdrop-blur-md">
|
| 146 |
+
<motion.div
|
| 147 |
+
initial={{ opacity: 0, y: -20 }}
|
| 148 |
+
animate={{ opacity: 1, y: 0 }}
|
| 149 |
+
exit={{ opacity: 0, y: -20 }}
|
| 150 |
+
className="w-full max-w-xl bg-white rounded-2xl shadow-2xl overflow-hidden border border-gray-100"
|
| 151 |
+
ref={searchRef}
|
| 152 |
+
>
|
| 153 |
+
<div className="p-4 border-b border-gray-100 flex items-center gap-3">
|
| 154 |
+
<Search className="text-gs-gold" size={20} />
|
| 155 |
+
<input
|
| 156 |
+
autoFocus
|
| 157 |
+
type="text"
|
| 158 |
+
value={query}
|
| 159 |
+
onChange={(e) => setQuery(e.target.value)}
|
| 160 |
+
placeholder="Search company, ticker, or crypto..."
|
| 161 |
+
className="flex-1 bg-transparent border-none outline-none text-gs-navy text-lg placeholder:text-gray-300"
|
| 162 |
+
/>
|
| 163 |
+
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors text-gs-slate">
|
| 164 |
+
<X size={20} />
|
| 165 |
+
</button>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<div className="max-h-[500px] overflow-y-auto">
|
| 169 |
+
{selectedStock ? (
|
| 170 |
+
<motion.div
|
| 171 |
+
initial={{ opacity: 0, x: 20 }}
|
| 172 |
+
animate={{ opacity: 1, x: 0 }}
|
| 173 |
+
className="p-8"
|
| 174 |
+
>
|
| 175 |
+
<div className="flex items-center gap-4 mb-8">
|
| 176 |
+
<div className="w-16 h-16 bg-gs-navy text-white rounded-2xl flex items-center justify-center text-2xl font-bold">
|
| 177 |
+
{selectedStock.symbol.charAt(0)}
|
| 178 |
+
</div>
|
| 179 |
+
<div>
|
| 180 |
+
<h3 className="text-2xl font-bold text-gs-navy">{selectedStock.symbol}</h3>
|
| 181 |
+
<p className="text-gs-slate">{selectedStock.shortname || selectedStock.longname}</p>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
| 186 |
+
<div className="bg-gs-light p-4 rounded-xl">
|
| 187 |
+
<label className="text-xs font-bold text-gs-slate uppercase tracking-wider block mb-2">Quantity (Shares)</label>
|
| 188 |
+
<input
|
| 189 |
+
type="number"
|
| 190 |
+
value={shares}
|
| 191 |
+
onChange={(e) => setShares(e.target.value)}
|
| 192 |
+
min="1"
|
| 193 |
+
className="w-full bg-white border border-gray-200 rounded-lg py-2 px-3 text-lg font-bold text-gs-navy focus:outline-none focus:ring-2 focus:ring-gs-gold"
|
| 194 |
+
/>
|
| 195 |
+
</div>
|
| 196 |
+
<div className="bg-gs-navy p-4 rounded-xl flex flex-col justify-center">
|
| 197 |
+
<p className="text-[10px] font-bold text-gs-gold uppercase tracking-widest mb-1">Trailing 3-Year Return</p>
|
| 198 |
+
<p className={`text-2xl font-bold ${performanceData.threeYearReturn >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
| 199 |
+
{performanceData.threeYearReturn >= 0 ? '+' : ''}{performanceData.threeYearReturn.toFixed(2)}%
|
| 200 |
+
</p>
|
| 201 |
+
<p className="text-[10px] text-gs-gold/60 mt-1 italic">Historical market performance</p>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div className="bg-gs-navy text-white p-6 rounded-2xl mb-8 relative overflow-hidden min-h-[120px] flex flex-col justify-center">
|
| 206 |
+
<div className="relative z-10">
|
| 207 |
+
<div className="flex items-center gap-2 mb-3 text-gs-gold">
|
| 208 |
+
<ShieldCheck size={18} />
|
| 209 |
+
<span className="text-xs font-bold uppercase tracking-widest">Portfolio Impact Analysis</span>
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
{isAnalyzing ? (
|
| 213 |
+
<div className="flex items-center gap-3">
|
| 214 |
+
<Loader2 size={16} className="animate-spin" />
|
| 215 |
+
<span className="text-sm font-light italic">Committee is calculating risk shifts...</span>
|
| 216 |
+
</div>
|
| 217 |
+
) : impactAnalysis ? (
|
| 218 |
+
<p className="text-sm font-light leading-relaxed animate-in fade-in slide-in-from-bottom-2 duration-500">
|
| 219 |
+
{impactAnalysis}
|
| 220 |
+
</p>
|
| 221 |
+
) : (
|
| 222 |
+
<button
|
| 223 |
+
type="button"
|
| 224 |
+
onClick={() => {
|
| 225 |
+
console.log("Analyze button clicked", { selectedStock, shares });
|
| 226 |
+
handleAnalyze();
|
| 227 |
+
}}
|
| 228 |
+
className="flex items-center gap-2 bg-gs-gold text-gs-navy px-4 py-2 rounded-lg text-xs font-bold hover:bg-white transition-all shadow-lg active:scale-95"
|
| 229 |
+
>
|
| 230 |
+
<PlayCircle size={14} />
|
| 231 |
+
Generate Impact Analysis
|
| 232 |
+
</button>
|
| 233 |
+
)}
|
| 234 |
+
</div>
|
| 235 |
+
<TrendingUp className="absolute -right-4 -bottom-4 text-white/5" size={120} />
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<div className="flex gap-4">
|
| 239 |
+
<button
|
| 240 |
+
onClick={() => setSelectedStock(null)}
|
| 241 |
+
className="flex-1 py-4 rounded-xl border border-gray-200 text-gs-slate font-medium hover:bg-gray-50 transition-all"
|
| 242 |
+
>
|
| 243 |
+
Back to Search
|
| 244 |
+
</button>
|
| 245 |
+
<button
|
| 246 |
+
onClick={handleConfirm}
|
| 247 |
+
className="flex-[2] py-4 rounded-xl bg-gs-navy text-white font-bold hover:bg-gs-gold hover:text-gs-navy transition-all shadow-lg hover:shadow-gs-gold/20"
|
| 248 |
+
>
|
| 249 |
+
Confirm & Add to Portfolio
|
| 250 |
+
</button>
|
| 251 |
+
</div>
|
| 252 |
+
</motion.div>
|
| 253 |
+
) : (
|
| 254 |
+
<>
|
| 255 |
+
{loading && query.length > 1 && (
|
| 256 |
+
<div className="p-8 text-center text-gs-slate flex flex-col items-center gap-2">
|
| 257 |
+
<Loader2 className="animate-spin text-gs-gold" size={24} />
|
| 258 |
+
<span className="text-sm font-light">Searching global markets...</span>
|
| 259 |
+
</div>
|
| 260 |
+
)}
|
| 261 |
+
|
| 262 |
+
{!loading && suggestions.length > 0 && (
|
| 263 |
+
<div className="py-2">
|
| 264 |
+
{suggestions.map((quote, idx) => (
|
| 265 |
+
<button
|
| 266 |
+
key={idx}
|
| 267 |
+
onClick={() => handleSelect(quote)}
|
| 268 |
+
className="w-full flex items-center justify-between px-6 py-4 hover:bg-gs-light transition-all text-left group"
|
| 269 |
+
>
|
| 270 |
+
<div className="flex items-center gap-4">
|
| 271 |
+
<div className={`w-10 h-10 rounded-lg flex items-center justify-center text-gs-navy group-hover:bg-white transition-colors ${quote.quoteType === 'MUTUALFUND' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100'}`}>
|
| 272 |
+
{quote.quoteType === 'MUTUALFUND' ? <Globe size={18} /> : <Briefcase size={18} />}
|
| 273 |
+
</div>
|
| 274 |
+
<div>
|
| 275 |
+
<div className="flex items-center gap-2">
|
| 276 |
+
<span className="font-bold text-gs-navy">{quote.symbol}</span>
|
| 277 |
+
<span className={`text-[8px] font-bold px-1.5 py-0.5 rounded uppercase tracking-tighter ${quote.quoteType === 'MUTUALFUND' ? 'bg-purple-500 text-white' : 'bg-gs-navy text-gs-gold'}`}>
|
| 278 |
+
{quote.quoteType === 'MUTUALFUND' ? 'Mutual Fund' : 'Stock'}
|
| 279 |
+
</span>
|
| 280 |
+
</div>
|
| 281 |
+
<div className="text-xs text-gs-slate truncate max-w-[250px]">
|
| 282 |
+
{quote.shortname || quote.longname}
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
<div className="flex items-center gap-2">
|
| 287 |
+
<span className="text-[10px] font-bold bg-gray-100 text-gray-500 px-2 py-1 rounded uppercase tracking-wider">
|
| 288 |
+
{quote.exchDisp}
|
| 289 |
+
</span>
|
| 290 |
+
<Plus size={16} className="text-gs-gold opacity-0 group-hover:opacity-100 transition-opacity" />
|
| 291 |
+
</div>
|
| 292 |
+
</button>
|
| 293 |
+
))}
|
| 294 |
+
</div>
|
| 295 |
+
)}
|
| 296 |
+
|
| 297 |
+
{!loading && query.length > 1 && suggestions.length === 0 && (
|
| 298 |
+
<div className="p-12 text-center text-gs-slate font-light">
|
| 299 |
+
No assets found for "{query}"
|
| 300 |
+
</div>
|
| 301 |
+
)}
|
| 302 |
+
|
| 303 |
+
{!query && (
|
| 304 |
+
<div className="p-8 text-center text-gs-slate">
|
| 305 |
+
<p className="text-sm">Try searching for <span className="font-medium text-gs-navy">"Apple"</span>, <span className="font-medium text-gs-navy">"NVDA"</span>, or <span className="font-medium text-gs-navy">"Bitcoin"</span></p>
|
| 306 |
+
</div>
|
| 307 |
+
)}
|
| 308 |
+
</>
|
| 309 |
+
)}
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<div className="p-3 bg-gs-light text-[10px] text-center text-gs-slate uppercase tracking-widest font-medium border-t border-gray-100">
|
| 313 |
+
Powered by Yahoo Finance Real-time Search
|
| 314 |
+
</div>
|
| 315 |
+
</motion.div>
|
| 316 |
+
</div>
|
| 317 |
+
);
|
| 318 |
+
}
|
src/components/TransparencyModal.jsx
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
+
import { X, Info, DollarSign, Shield } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
export default function TransparencyModal({ isOpen, onClose, data, currentPortfolio, totalValue, prices }) {
|
| 6 |
+
if (!isOpen || !data) return null;
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<AnimatePresence>
|
| 10 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
| 11 |
+
{/* Backdrop */}
|
| 12 |
+
<motion.div
|
| 13 |
+
initial={{ opacity: 0 }}
|
| 14 |
+
animate={{ opacity: 1 }}
|
| 15 |
+
exit={{ opacity: 0 }}
|
| 16 |
+
onClick={onClose}
|
| 17 |
+
className="absolute inset-0 bg-gs-navy/60 backdrop-blur-sm"
|
| 18 |
+
/>
|
| 19 |
+
|
| 20 |
+
{/* Modal Content */}
|
| 21 |
+
<motion.div
|
| 22 |
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 23 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 24 |
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 25 |
+
className="relative bg-white rounded-3xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-hidden border border-gray-100 flex flex-col"
|
| 26 |
+
>
|
| 27 |
+
{/* Header (Fixed at top) */}
|
| 28 |
+
<div className="bg-gs-navy p-6 flex justify-between items-start text-white shrink-0">
|
| 29 |
+
<div>
|
| 30 |
+
<span className="text-gs-gold text-xs font-semibold uppercase tracking-wider mb-2 block">Recommendation</span>
|
| 31 |
+
<h2 className="text-2xl font-light">{data.title}</h2>
|
| 32 |
+
</div>
|
| 33 |
+
<button onClick={onClose} className="text-white/60 hover:text-white transition-colors">
|
| 34 |
+
<X size={24} />
|
| 35 |
+
</button>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div className="p-8 space-y-6 overflow-y-auto scrollbar-hide">
|
| 39 |
+
{/* The Advice */}
|
| 40 |
+
<div className="bg-gs-light p-5 rounded-xl border-l-4 border-gs-gold">
|
| 41 |
+
<h3 className="font-medium text-gs-navy mb-1 flex items-center">
|
| 42 |
+
<Info size={18} className="mr-2 text-gs-gold" /> The Action Plan
|
| 43 |
+
</h3>
|
| 44 |
+
<p className="text-gs-slate font-light text-lg">
|
| 45 |
+
{(() => {
|
| 46 |
+
let advice = data.advice;
|
| 47 |
+
const hasBonds = currentPortfolio?.allocation?.some(a =>
|
| 48 |
+
a.ticker?.includes('BND') ||
|
| 49 |
+
a.name?.toLowerCase().includes('bond')
|
| 50 |
+
);
|
| 51 |
+
if (!hasBonds && advice.includes('Bonds')) {
|
| 52 |
+
advice = advice.replace('Bonds', 'Cash Reserves');
|
| 53 |
+
}
|
| 54 |
+
return advice;
|
| 55 |
+
})()}
|
| 56 |
+
</p>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
{/* Visual Comparison Section */}
|
| 60 |
+
<div className="bg-gs-light/50 p-6 rounded-2xl border border-gray-100 min-h-[100px] flex flex-col justify-center">
|
| 61 |
+
<h3 className="text-xs uppercase tracking-widest text-gs-slate font-semibold mb-4">
|
| 62 |
+
Visual Rebalance Suggestion
|
| 63 |
+
</h3>
|
| 64 |
+
|
| 65 |
+
<div className="space-y-6">
|
| 66 |
+
{currentPortfolio?.allocation?.length > 0 ? (
|
| 67 |
+
currentPortfolio.allocation.slice(0, 3).map((asset, idx) => {
|
| 68 |
+
if (!asset) return null;
|
| 69 |
+
|
| 70 |
+
// Mock logic for "Target" based on scenario
|
| 71 |
+
let change = 0;
|
| 72 |
+
const trigger = data.trigger || "";
|
| 73 |
+
const assetName = asset.name || "";
|
| 74 |
+
|
| 75 |
+
if (trigger.includes("Market Drop")) {
|
| 76 |
+
if (assetName.includes("Bond") || assetName.includes("Cash")) change = -5;
|
| 77 |
+
else change = 5;
|
| 78 |
+
} else if (trigger.includes("Life Expense")) {
|
| 79 |
+
if (assetName.includes("Cash")) change = 20;
|
| 80 |
+
else change = -10;
|
| 81 |
+
} else if (trigger.includes("Inflation")) {
|
| 82 |
+
if (assetName.includes("Vanguard") || assetName.includes("Value")) change = 8;
|
| 83 |
+
else change = -4;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
const val = Number(asset.value) || 0;
|
| 87 |
+
const targetValue = Math.max(0, Math.min(100, val + change));
|
| 88 |
+
|
| 89 |
+
// Calculate Dollar and Share Changes
|
| 90 |
+
const ticker = asset.ticker;
|
| 91 |
+
const priceObj = prices[ticker];
|
| 92 |
+
const price = priceObj?.price;
|
| 93 |
+
|
| 94 |
+
// Use a fallback total value if current calculation is still in flight
|
| 95 |
+
const activeTotal = totalValue > 0 ? totalValue : 50000;
|
| 96 |
+
|
| 97 |
+
const currentValue = (val / 100) * activeTotal;
|
| 98 |
+
const targetValueDollar = (targetValue / 100) * activeTotal;
|
| 99 |
+
const dollarDiff = targetValueDollar - currentValue;
|
| 100 |
+
const sharesDiff = price ? Math.abs(Math.round(dollarDiff / price)) : null;
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
<div key={idx} className="space-y-2">
|
| 104 |
+
<div className="flex justify-between text-xs">
|
| 105 |
+
<span className="font-bold text-gs-navy">{ticker}</span>
|
| 106 |
+
<div className="text-right">
|
| 107 |
+
<span className="text-gs-slate">
|
| 108 |
+
{val.toFixed(2)}% <span className="mx-2">→</span>
|
| 109 |
+
<span className={change > 0 ? 'text-green-600' : change < 0 ? 'text-red-500' : 'text-gs-navy'}>
|
| 110 |
+
{targetValue.toFixed(2)}%
|
| 111 |
+
</span>
|
| 112 |
+
</span>
|
| 113 |
+
<p className={`text-[10px] font-bold ${change > 0 ? 'text-green-600' : 'text-red-500'}`}>
|
| 114 |
+
{change > 0 ? 'Buy' : 'Sell'} ${Math.abs(Math.round(dollarDiff)).toLocaleString()}
|
| 115 |
+
{sharesDiff !== null ? ` (${sharesDiff} shares)` : ' (Calculating...)'}
|
| 116 |
+
</p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
<div className="h-3 w-full bg-gray-200 rounded-full overflow-hidden flex relative">
|
| 120 |
+
<div
|
| 121 |
+
className="h-full bg-gs-navy opacity-30"
|
| 122 |
+
style={{ width: `${val}%` }}
|
| 123 |
+
></div>
|
| 124 |
+
<div
|
| 125 |
+
className={`h-full absolute top-0 left-0 transition-all duration-1000 ${change >= 0 ? 'bg-green-500' : 'bg-red-500'}`}
|
| 126 |
+
style={{ width: `${targetValue}%` }}
|
| 127 |
+
></div>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
);
|
| 131 |
+
})
|
| 132 |
+
) : (
|
| 133 |
+
<div className="text-center py-4">
|
| 134 |
+
<p className="text-xs text-gs-slate italic">Add at least one stock to see a visual rebalance simulation.</p>
|
| 135 |
+
</div>
|
| 136 |
+
)}
|
| 137 |
+
</div>
|
| 138 |
+
<p className="text-[10px] text-gs-slate mt-4 italic text-center">
|
| 139 |
+
*Simulated shift based on your {currentPortfolio?.riskLevel || 'selected'} risk profile.
|
| 140 |
+
</p>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
{/* The Why */}
|
| 144 |
+
<div>
|
| 145 |
+
<h3 className="font-medium text-gs-navy mb-2 flex items-center">
|
| 146 |
+
<Shield size={18} className="mr-2 text-gs-slate" /> Why we recommend this
|
| 147 |
+
</h3>
|
| 148 |
+
<p className="text-gs-slate font-light text-sm leading-relaxed">
|
| 149 |
+
{data.explanation}
|
| 150 |
+
</p>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<hr className="border-gray-100" />
|
| 154 |
+
|
| 155 |
+
{/* Radical Transparency Section */}
|
| 156 |
+
<div>
|
| 157 |
+
<h3 className="text-xs uppercase tracking-widest text-gs-slate font-semibold mb-4">
|
| 158 |
+
Full Transparency
|
| 159 |
+
</h3>
|
| 160 |
+
|
| 161 |
+
<div className="space-y-3">
|
| 162 |
+
<div className="flex justify-between items-center p-3 rounded-lg border border-gray-100 bg-white">
|
| 163 |
+
<span className="text-sm text-gs-slate font-light flex items-center">
|
| 164 |
+
<DollarSign size={14} className="mr-2 text-gray-400" /> Fee Impact
|
| 165 |
+
</span>
|
| 166 |
+
<span className="font-medium text-gs-navy text-sm">{data.feeImpactDollars}</span>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div className="flex justify-between items-center p-3 rounded-lg border border-gray-100 bg-white">
|
| 170 |
+
<span className="text-sm text-gs-slate font-light flex items-center">
|
| 171 |
+
<Shield size={14} className="mr-2 text-gray-400" /> Tax Considerations
|
| 172 |
+
</span>
|
| 173 |
+
<span className="font-medium text-gs-navy text-sm">{data.taxImpact}</span>
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<button
|
| 179 |
+
onClick={onClose}
|
| 180 |
+
className="w-full mt-4 bg-gs-navy text-white py-4 rounded-xl font-medium hover:bg-gs-navy/90 transition-colors shadow-md shrink-0"
|
| 181 |
+
>
|
| 182 |
+
I Understand
|
| 183 |
+
</button>
|
| 184 |
+
</div>
|
| 185 |
+
</motion.div>
|
| 186 |
+
</div>
|
| 187 |
+
</AnimatePresence>
|
| 188 |
+
);
|
| 189 |
+
}
|
src/data/historicalData.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { tickerMetrics } from './tickerMetrics';
|
| 2 |
+
|
| 3 |
+
// Helper to generate a realistic random walk for a stock price
|
| 4 |
+
const generateRandomWalk = (startPrice, days, volatility) => {
|
| 5 |
+
const data = [];
|
| 6 |
+
let currentPrice = startPrice;
|
| 7 |
+
const now = new Date();
|
| 8 |
+
|
| 9 |
+
for (let i = days; i >= 0; i--) {
|
| 10 |
+
const date = new Date(now);
|
| 11 |
+
date.setDate(date.getDate() - i);
|
| 12 |
+
|
| 13 |
+
// Random daily return based on volatility (standard deviation)
|
| 14 |
+
const dailyReturn = (Math.random() - 0.5) * volatility;
|
| 15 |
+
currentPrice = currentPrice * (1 + dailyReturn);
|
| 16 |
+
|
| 17 |
+
data.push({
|
| 18 |
+
time: date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }),
|
| 19 |
+
date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
| 20 |
+
price: Number(currentPrice.toFixed(2))
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
return data;
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
// Base starting prices for realism
|
| 27 |
+
const basePrices = {
|
| 28 |
+
'SPY': 510.50,
|
| 29 |
+
'VOO': 468.20,
|
| 30 |
+
'BND': 72.15,
|
| 31 |
+
'BNDX': 48.90,
|
| 32 |
+
'AAPL': 175.40,
|
| 33 |
+
'MSFT': 420.10,
|
| 34 |
+
'GOOGL': 155.30,
|
| 35 |
+
'AMZN': 180.25,
|
| 36 |
+
'JNJ': 155.60,
|
| 37 |
+
'BRK-B': 410.80,
|
| 38 |
+
'JPM': 195.40,
|
| 39 |
+
'VXUS': 58.70,
|
| 40 |
+
'TSLA': 175.20,
|
| 41 |
+
'QQQ': 440.50,
|
| 42 |
+
'VTI': 255.60,
|
| 43 |
+
'CASH': 1.00
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const mockDataCache = {};
|
| 47 |
+
|
| 48 |
+
export const fetchRealData = async (ticker) => {
|
| 49 |
+
if (ticker === 'CASH') return getHistoricalData('CASH');
|
| 50 |
+
|
| 51 |
+
try {
|
| 52 |
+
const proxy = 'https://corsproxy.io/?';
|
| 53 |
+
const url = `${proxy}https://query1.finance.yahoo.com/v8/finance/chart/${ticker}?interval=1d&range=1y`;
|
| 54 |
+
const response = await fetch(url);
|
| 55 |
+
const data = await response.json();
|
| 56 |
+
|
| 57 |
+
if (!data.chart?.result?.[0]) throw new Error('Invalid ticker');
|
| 58 |
+
|
| 59 |
+
const result = data.chart.result[0];
|
| 60 |
+
const timestamps = result.timestamp;
|
| 61 |
+
const quotes = result.indicators.quote[0].close;
|
| 62 |
+
|
| 63 |
+
const history = timestamps.map((ts, i) => {
|
| 64 |
+
const date = new Date(ts * 1000);
|
| 65 |
+
return {
|
| 66 |
+
time: date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }),
|
| 67 |
+
date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
| 68 |
+
price: Number(quotes[i]?.toFixed(2)) || 0
|
| 69 |
+
};
|
| 70 |
+
}).filter(d => d.price > 0);
|
| 71 |
+
|
| 72 |
+
const currentPrice = history[history.length - 1].price;
|
| 73 |
+
const oldPrice = history[0].price;
|
| 74 |
+
const change = currentPrice - oldPrice;
|
| 75 |
+
const percentChange = (change / oldPrice) * 100;
|
| 76 |
+
|
| 77 |
+
const finalData = {
|
| 78 |
+
ticker,
|
| 79 |
+
currentPrice: Number(currentPrice.toFixed(2)),
|
| 80 |
+
change: Number(change.toFixed(2)),
|
| 81 |
+
percentChange: Number(percentChange.toFixed(2)),
|
| 82 |
+
isPositive: change >= 0,
|
| 83 |
+
history
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
mockDataCache[ticker] = finalData;
|
| 87 |
+
return finalData;
|
| 88 |
+
} catch (error) {
|
| 89 |
+
console.error("Yahoo Finance Error:", error);
|
| 90 |
+
return getHistoricalData(ticker); // Fallback to mock
|
| 91 |
+
}
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
export const getHistoricalData = (ticker) => {
|
| 95 |
+
if (mockDataCache[ticker]) {
|
| 96 |
+
return mockDataCache[ticker];
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const startPrice = basePrices[ticker] || 100;
|
| 100 |
+
const metrics = tickerMetrics[ticker] || { beta: 1 };
|
| 101 |
+
const volatility = (metrics.beta * 0.015);
|
| 102 |
+
|
| 103 |
+
const history = generateRandomWalk(startPrice, 365, volatility);
|
| 104 |
+
const currentPrice = history[history.length - 1].price;
|
| 105 |
+
const oldPrice = history[0].price;
|
| 106 |
+
const change = currentPrice - oldPrice;
|
| 107 |
+
const percentChange = (change / oldPrice) * 100;
|
| 108 |
+
|
| 109 |
+
const result = {
|
| 110 |
+
ticker,
|
| 111 |
+
currentPrice: Number(currentPrice.toFixed(2)),
|
| 112 |
+
change: Number(change.toFixed(2)),
|
| 113 |
+
percentChange: Number(percentChange.toFixed(2)),
|
| 114 |
+
isPositive: change >= 0,
|
| 115 |
+
history
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
mockDataCache[ticker] = result;
|
| 119 |
+
return result;
|
| 120 |
+
};
|
src/data/mockData.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const mockPortfolios = {
|
| 2 |
+
Cautious: {
|
| 3 |
+
allocation: [
|
| 4 |
+
{ name: 'Vanguard Total Bond (BND)', ticker: 'BND', value: 40, color: '#1E293B', shares: 145, dateBought: '2023-01-15', type: 'mf' },
|
| 5 |
+
{ name: 'Vanguard Intl Bond (BNDX)', ticker: 'BNDX', value: 20, color: '#334155', shares: 85, dateBought: '2023-03-22', type: 'mf' },
|
| 6 |
+
{ name: 'SPDR S&P 500 (SPY)', ticker: 'SPY', value: 15, color: '#C5A880', shares: 12, dateBought: '2022-11-10', type: 'mf' },
|
| 7 |
+
{ name: 'Johnson & Johnson (JNJ)', ticker: 'JNJ', value: 15, color: '#64748B', shares: 35, dateBought: '2023-05-05', type: 'stock' },
|
| 8 |
+
{ name: 'Cash Equivalents', ticker: 'CASH', value: 10, color: '#E5E7EB', shares: 2500, dateBought: '2024-01-01', type: 'stock' }
|
| 9 |
+
],
|
| 10 |
+
riskLevel: 'Low',
|
| 11 |
+
expectedReturn: '4-5%',
|
| 12 |
+
feeImpact: 'Low',
|
| 13 |
+
hiddenFees: { expenseRatio: 0.05, advisoryFee: 0.0, tradingCosts: 2.0 }
|
| 14 |
+
},
|
| 15 |
+
Balanced: {
|
| 16 |
+
allocation: [
|
| 17 |
+
{ name: 'Vanguard S&P 500 (VOO)', ticker: 'VOO', value: 30, color: '#0B233F', shares: 45, dateBought: '2022-08-14', type: 'mf' },
|
| 18 |
+
{ name: 'Microsoft Corp (MSFT)', ticker: 'MSFT', value: 15, color: '#C5A880', shares: 25, dateBought: '2021-12-01', type: 'stock' },
|
| 19 |
+
{ name: 'Apple Inc. (AAPL)', ticker: 'AAPL', value: 15, color: '#1E293B', shares: 60, dateBought: '2022-02-18', type: 'stock' },
|
| 20 |
+
{ name: 'Vanguard Total Intl (VXUS)', ticker: 'VXUS', value: 15, color: '#64748B', shares: 90, dateBought: '2023-06-30', type: 'mf' },
|
| 21 |
+
{ name: 'Berkshire Hathaway (BRK-B)', ticker: 'BRK-B', value: 10, color: '#334155', shares: 18, dateBought: '2022-05-12', type: 'stock' },
|
| 22 |
+
{ name: 'Vanguard Total Bond (BND)', ticker: 'BND', value: 15, color: '#94A3B8', shares: 70, dateBought: '2023-11-20', type: 'mf' }
|
| 23 |
+
],
|
| 24 |
+
riskLevel: 'Medium',
|
| 25 |
+
expectedReturn: '7-9%',
|
| 26 |
+
feeImpact: 'Medium',
|
| 27 |
+
hiddenFees: { expenseRatio: 0.04, advisoryFee: 0.0, tradingCosts: 10.0 }
|
| 28 |
+
},
|
| 29 |
+
Bold: {
|
| 30 |
+
allocation: [
|
| 31 |
+
{ name: 'Invesco QQQ Trust (QQQ)', ticker: 'QQQ', value: 25, color: '#0B233F', shares: 35, dateBought: '2021-09-10', type: 'mf' },
|
| 32 |
+
{ name: 'Tesla, Inc. (TSLA)', ticker: 'TSLA', value: 20, color: '#C5A880', shares: 40, dateBought: '2022-10-05', type: 'stock' },
|
| 33 |
+
{ name: 'Amazon.com (AMZN)', ticker: 'AMZN', value: 15, color: '#1E293B', shares: 55, dateBought: '2023-02-28', type: 'stock' },
|
| 34 |
+
{ name: 'Google (GOOGL)', ticker: 'GOOGL', value: 15, color: '#64748B', shares: 65, dateBought: '2023-04-14', type: 'stock' },
|
| 35 |
+
{ name: 'JPMorgan Chase (JPM)', ticker: 'JPM', value: 15, color: '#334155', shares: 42, dateBought: '2022-07-22', type: 'stock' },
|
| 36 |
+
{ name: 'Vanguard S&P 500 (VOO)', ticker: 'VOO', value: 10, color: '#94A3B8', shares: 15, dateBought: '2024-01-10', type: 'mf' }
|
| 37 |
+
],
|
| 38 |
+
riskLevel: 'High',
|
| 39 |
+
expectedReturn: '12-15%',
|
| 40 |
+
feeImpact: 'High',
|
| 41 |
+
hiddenFees: { expenseRatio: 0.12, advisoryFee: 0.0, tradingCosts: 25.0 }
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
export const rebalancingScenarios = {
|
| 46 |
+
marketDrop: {
|
| 47 |
+
trigger: "Market Drop of 10%+",
|
| 48 |
+
title: "Market Correction Strategy",
|
| 49 |
+
advice: "Reallocate 5% from Bonds to Equities (Buy the Dip).",
|
| 50 |
+
explanation: "During a drop of 10% or more, high-quality stocks go 'on sale'. We automatically shift a small portion of your stable bond holdings into equities to capture the eventual recovery. This is a standard, disciplined approach to buy low and sell high without timing the market.",
|
| 51 |
+
feeImpactDollars: "$2.50 (ETF Bid/Ask Spread)",
|
| 52 |
+
taxImpact: "None (Done within tax-advantaged accounts if applicable)"
|
| 53 |
+
},
|
| 54 |
+
inflation: {
|
| 55 |
+
trigger: "High Inflation (4%+)",
|
| 56 |
+
title: "Inflation Protection Shift",
|
| 57 |
+
advice: "Increase allocation to Value Stocks and International Equities.",
|
| 58 |
+
explanation: "When inflation runs hot, growth stocks often suffer while value companies (with current cash flows) and international equities can provide better protection. This shift acts as a shield to preserve your real purchasing power.",
|
| 59 |
+
feeImpactDollars: "$1.20 (Rebalancing Costs)",
|
| 60 |
+
taxImpact: "Minimal (Loss harvesting applied where possible)"
|
| 61 |
+
},
|
| 62 |
+
withdrawal: {
|
| 63 |
+
trigger: "Major Life Expense",
|
| 64 |
+
title: "Capital Preservation Mode",
|
| 65 |
+
advice: "Shift 20% of Equities into Cash Equivalents and Short-Term Bonds.",
|
| 66 |
+
explanation: "When you have a major life event coming up (like buying a house), you cannot afford short-term market volatility. We lock in your gains by shifting to highly liquid, stable assets so your money is guaranteed to be there when you need it.",
|
| 67 |
+
feeImpactDollars: "$0.00 (Zero-fee transaction)",
|
| 68 |
+
taxImpact: "Moderate (Capital gains realized; tax-loss harvesting offset attempted)"
|
| 69 |
+
}
|
| 70 |
+
};
|
src/data/tickerMetrics.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const tickerMetrics = {
|
| 2 |
+
'SPY': { divYield: 1.32, beta: 1.00, expenseRatio: 0.09, peRatio: 26.5 },
|
| 3 |
+
'VOO': { divYield: 1.34, beta: 1.00, expenseRatio: 0.03, peRatio: 26.5 },
|
| 4 |
+
'BND': { divYield: 3.25, beta: 0.12, expenseRatio: 0.03, peRatio: 0 },
|
| 5 |
+
'BNDX': { divYield: 2.50, beta: 0.15, expenseRatio: 0.07, peRatio: 0 },
|
| 6 |
+
'AAPL': { divYield: 0.48, beta: 1.24, expenseRatio: 0, peRatio: 31.2 },
|
| 7 |
+
'MSFT': { divYield: 0.70, beta: 0.90, expenseRatio: 0, peRatio: 35.5 },
|
| 8 |
+
'GOOGL': { divYield: 0, beta: 1.05, expenseRatio: 0, peRatio: 28.2 },
|
| 9 |
+
'AMZN': { divYield: 0, beta: 1.15, expenseRatio: 0, peRatio: 42.1 },
|
| 10 |
+
'JNJ': { divYield: 3.00, beta: 0.55, expenseRatio: 0, peRatio: 18.5 },
|
| 11 |
+
'BRK-B': { divYield: 0, beta: 0.85, expenseRatio: 0, peRatio: 22.4 },
|
| 12 |
+
'JPM': { divYield: 2.30, beta: 1.10, expenseRatio: 0, peRatio: 12.5 },
|
| 13 |
+
'VXUS': { divYield: 3.12, beta: 1.05, expenseRatio: 0.07, peRatio: 15.4 },
|
| 14 |
+
'TSLA': { divYield: 0, beta: 2.31, expenseRatio: 0, peRatio: 72.8 },
|
| 15 |
+
'QQQ': { divYield: 0.61, beta: 1.18, expenseRatio: 0.20, peRatio: 35.1 },
|
| 16 |
+
'VTI': { divYield: 1.35, beta: 1.00, expenseRatio: 0.03, peRatio: 25.8 },
|
| 17 |
+
'CASH': { divYield: 4.50, beta: 0, expenseRatio: 0, peRatio: 0 },
|
| 18 |
+
};
|
src/index.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Outfit:wght@100..900&display=swap');
|
| 2 |
+
|
| 3 |
+
@tailwind base;
|
| 4 |
+
@tailwind components;
|
| 5 |
+
@tailwind utilities;
|
| 6 |
+
|
| 7 |
+
@layer base {
|
| 8 |
+
body {
|
| 9 |
+
@apply bg-gs-light text-gs-navy font-sans antialiased;
|
| 10 |
+
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
h1, h2, h3, h4, h5, h6 {
|
| 14 |
+
font-family: 'Outfit', sans-serif;
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/* Custom styles for animations or specific components if needed */
|
| 19 |
+
.glass-panel {
|
| 20 |
+
@apply bg-white/80 backdrop-blur-md border border-white/20 shadow-xl rounded-2xl;
|
| 21 |
+
}
|
src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.jsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
src/pages/Dashboard.jsx
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useMemo, useEffect } from 'react';
|
| 2 |
+
import { fetchRealData } from '../data/historicalData';
|
| 3 |
+
import { mockPortfolios } from '../data/mockData';
|
| 4 |
+
import { tickerMetrics } from '../data/tickerMetrics';
|
| 5 |
+
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
|
| 6 |
+
import Indicators from '../components/Indicators';
|
| 7 |
+
import FeeTransparencyModule from '../components/FeeTransparencyModule';
|
| 8 |
+
import RebalancingEngine from '../components/RebalancingEngine';
|
| 9 |
+
import TransparencyModal from '../components/TransparencyModal';
|
| 10 |
+
import MacroTracker from '../components/MacroTracker';
|
| 11 |
+
import StockPopup from '../components/StockPopup';
|
| 12 |
+
import PortfolioHeatmap from '../components/PortfolioHeatmap';
|
| 13 |
+
import FinancialCalculators from '../components/FinancialCalculators';
|
| 14 |
+
import { LayoutGrid, Plus } from 'lucide-react';
|
| 15 |
+
import { AnimatePresence } from 'framer-motion';
|
| 16 |
+
import StockSearch from '../components/StockSearch';
|
| 17 |
+
|
| 18 |
+
export default function Dashboard({ riskProfile }) {
|
| 19 |
+
// Initialize with a cached portfolio if available, otherwise an empty one
|
| 20 |
+
const [currentPortfolio, setCurrentPortfolio] = useState(() => {
|
| 21 |
+
const saved = localStorage.getItem('gs_portfolio');
|
| 22 |
+
if (saved) {
|
| 23 |
+
try {
|
| 24 |
+
return JSON.parse(saved);
|
| 25 |
+
} catch (e) {
|
| 26 |
+
console.error("Error loading saved portfolio:", e);
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
return {
|
| 30 |
+
...mockPortfolios[riskProfile],
|
| 31 |
+
allocation: []
|
| 32 |
+
};
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Save to cache whenever portfolio changes
|
| 36 |
+
useEffect(() => {
|
| 37 |
+
localStorage.setItem('gs_portfolio', JSON.stringify(currentPortfolio));
|
| 38 |
+
}, [currentPortfolio]);
|
| 39 |
+
const [modalData, setModalData] = useState(null);
|
| 40 |
+
|
| 41 |
+
// State for popups
|
| 42 |
+
const [activePopupAsset, setActivePopupAsset] = useState(null);
|
| 43 |
+
const [showHeatmap, setShowHeatmap] = useState(false);
|
| 44 |
+
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
| 45 |
+
const [totals, setTotals] = useState({ value: 0, change: 0, percent: 0, rawValue: 0, loading: true });
|
| 46 |
+
const [prices, setPrices] = useState({});
|
| 47 |
+
|
| 48 |
+
// Dynamic Calculation of Health, Risk, and Weights based on real metrics
|
| 49 |
+
const displayPortfolio = useMemo(() => {
|
| 50 |
+
let totalBeta = 0;
|
| 51 |
+
let totalExpense = 0;
|
| 52 |
+
const numAssets = currentPortfolio.allocation.length;
|
| 53 |
+
|
| 54 |
+
// Calculate current market total
|
| 55 |
+
let marketTotal = 0;
|
| 56 |
+
currentPortfolio.allocation.forEach(asset => {
|
| 57 |
+
const priceData = prices[asset.ticker] || { price: 0 };
|
| 58 |
+
marketTotal += asset.shares * priceData.price;
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
const updatedAllocation = currentPortfolio.allocation.map(asset => {
|
| 62 |
+
const priceData = prices[asset.ticker] || { price: 0, percent: 0 };
|
| 63 |
+
const marketVal = asset.shares * priceData.price;
|
| 64 |
+
const weight = marketTotal > 0 ? (marketVal / marketTotal) * 100 : 0;
|
| 65 |
+
|
| 66 |
+
const metrics = tickerMetrics[asset.ticker] || { beta: 1, expenseRatio: 0.1 };
|
| 67 |
+
totalBeta += (metrics.beta || 1) * (weight / 100);
|
| 68 |
+
totalExpense += (metrics.expenseRatio || 0.1) * (weight / 100);
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
...asset,
|
| 72 |
+
value: Number(weight.toFixed(2)),
|
| 73 |
+
dayChange: priceData.percent,
|
| 74 |
+
dollarChange: priceData.change
|
| 75 |
+
};
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
if (numAssets === 0) {
|
| 79 |
+
return {
|
| 80 |
+
...currentPortfolio,
|
| 81 |
+
healthScore: 0,
|
| 82 |
+
riskLevel: 'None',
|
| 83 |
+
avgBeta: '0.00',
|
| 84 |
+
stockSplit: 0,
|
| 85 |
+
mfSplit: 0
|
| 86 |
+
};
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Determine Risk Level
|
| 90 |
+
let riskLevel = 'Medium';
|
| 91 |
+
if (totalBeta < 0.6) riskLevel = 'Low';
|
| 92 |
+
else if (totalBeta > 1.2) riskLevel = 'High';
|
| 93 |
+
|
| 94 |
+
// Determine Health Score (Base 100)
|
| 95 |
+
let healthScore = 100;
|
| 96 |
+
healthScore -= Math.min(totalExpense * 100 * 2, 20);
|
| 97 |
+
if (numAssets >= 5) healthScore += 5;
|
| 98 |
+
|
| 99 |
+
const profileMap = { 'Cautious': 'Low', 'Balanced': 'Medium', 'Bold': 'High' };
|
| 100 |
+
if (riskLevel !== profileMap[riskProfile]) healthScore -= 15;
|
| 101 |
+
healthScore = Math.min(Math.max(Math.round(healthScore), 0), 100);
|
| 102 |
+
|
| 103 |
+
// Calculate Asset Type Split
|
| 104 |
+
let stockWeight = 0;
|
| 105 |
+
let mfWeight = 0;
|
| 106 |
+
updatedAllocation.forEach(asset => {
|
| 107 |
+
if (asset.type === 'mf') mfWeight += asset.value;
|
| 108 |
+
else stockWeight += asset.value;
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
return {
|
| 112 |
+
...currentPortfolio,
|
| 113 |
+
allocation: updatedAllocation,
|
| 114 |
+
healthScore,
|
| 115 |
+
riskLevel,
|
| 116 |
+
avgBeta: totalBeta.toFixed(2),
|
| 117 |
+
stockSplit: stockWeight,
|
| 118 |
+
mfSplit: mfWeight
|
| 119 |
+
};
|
| 120 |
+
}, [currentPortfolio, riskProfile, prices]);
|
| 121 |
+
|
| 122 |
+
const handleRebalance = (scenario) => {
|
| 123 |
+
setModalData(scenario);
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
const closeModal = () => setModalData(null);
|
| 127 |
+
const closeStockPopup = () => setActivePopupAsset(null);
|
| 128 |
+
|
| 129 |
+
const handleAddStock = (newAsset) => {
|
| 130 |
+
setCurrentPortfolio(prev => {
|
| 131 |
+
// If portfolio is empty, the first stock gets 100% allocation
|
| 132 |
+
if (prev.allocation.length === 0) {
|
| 133 |
+
return {
|
| 134 |
+
...prev,
|
| 135 |
+
allocation: [{ ...newAsset, value: 100 }]
|
| 136 |
+
};
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const scale = (100 - newAsset.value) / 100;
|
| 140 |
+
const updatedExisting = prev.allocation.map(a => ({
|
| 141 |
+
...a,
|
| 142 |
+
value: Number((a.value * scale).toFixed(2))
|
| 143 |
+
}));
|
| 144 |
+
|
| 145 |
+
// Ensure sum is exactly 100
|
| 146 |
+
const currentSum = updatedExisting.reduce((acc, a) => acc + a.value, 0) + newAsset.value;
|
| 147 |
+
if (currentSum !== 100 && updatedExisting.length > 0) {
|
| 148 |
+
updatedExisting[0].value += Number((100 - currentSum).toFixed(2));
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
...prev,
|
| 153 |
+
allocation: [...updatedExisting, newAsset]
|
| 154 |
+
};
|
| 155 |
+
});
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
useEffect(() => {
|
| 159 |
+
async function calculateTotals() {
|
| 160 |
+
if (currentPortfolio.allocation.length === 0) {
|
| 161 |
+
setTotals({ value: '$0.00', change: '$0.00', percent: '0.00', isPositive: true, loading: false });
|
| 162 |
+
return;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
setTotals(prev => ({ ...prev, loading: true }));
|
| 166 |
+
|
| 167 |
+
const pricePromises = currentPortfolio.allocation.map(asset => fetchRealData(asset.ticker));
|
| 168 |
+
const results = await Promise.all(pricePromises);
|
| 169 |
+
|
| 170 |
+
let newTotalValue = 0;
|
| 171 |
+
let newTotalChange = 0;
|
| 172 |
+
const priceMap = {};
|
| 173 |
+
|
| 174 |
+
results.forEach((data, index) => {
|
| 175 |
+
const ticker = currentPortfolio.allocation[index].ticker;
|
| 176 |
+
priceMap[ticker] = {
|
| 177 |
+
price: data.currentPrice,
|
| 178 |
+
change: data.change,
|
| 179 |
+
percent: (data.change / (data.currentPrice - data.change)) * 100
|
| 180 |
+
};
|
| 181 |
+
newTotalValue += currentPortfolio.allocation[index].shares * data.currentPrice;
|
| 182 |
+
newTotalChange += (currentPortfolio.allocation[index].shares * data.change);
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
setPrices(priceMap);
|
| 186 |
+
|
| 187 |
+
const percent = newTotalValue > 0 ? (newTotalChange / (newTotalValue - newTotalChange)) * 100 : 0;
|
| 188 |
+
|
| 189 |
+
// Update totals
|
| 190 |
+
setTotals({
|
| 191 |
+
value: newTotalValue.toLocaleString('en-US', { style: 'currency', currency: 'USD' }),
|
| 192 |
+
change: newTotalChange.toLocaleString('en-US', { style: 'currency', currency: 'USD' }),
|
| 193 |
+
percent: percent.toFixed(2),
|
| 194 |
+
rawValue: newTotalValue,
|
| 195 |
+
isPositive: newTotalChange >= 0,
|
| 196 |
+
loading: false
|
| 197 |
+
});
|
| 198 |
+
}
|
| 199 |
+
calculateTotals();
|
| 200 |
+
}, [JSON.stringify(currentPortfolio.allocation.map(a => `${a.ticker}-${a.shares}`))]); // Only watch ticker/shares, not the derived weights changes
|
| 201 |
+
|
| 202 |
+
return (
|
| 203 |
+
<div className="min-h-screen bg-gs-light p-6 md:p-12 relative">
|
| 204 |
+
<div className="max-w-7xl mx-auto">
|
| 205 |
+
<header className="mb-10 flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
|
| 206 |
+
<div>
|
| 207 |
+
<h1 className="text-3xl md:text-4xl font-light text-gs-navy mb-2">
|
| 208 |
+
Your <span className="font-semibold">{riskProfile}</span> Portfolio
|
| 209 |
+
</h1>
|
| 210 |
+
<p className="text-gs-slate text-lg font-light">
|
| 211 |
+
Built for your goals. Transparently managed.
|
| 212 |
+
</p>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<div className="bg-white px-8 py-4 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-8 min-w-[320px]">
|
| 216 |
+
<div>
|
| 217 |
+
<p className="text-[10px] font-bold text-gs-slate uppercase tracking-widest mb-1">Total Value</p>
|
| 218 |
+
{totals.loading ? (
|
| 219 |
+
<div className="h-8 w-24 bg-gray-100 animate-pulse rounded"></div>
|
| 220 |
+
) : (
|
| 221 |
+
<p className="text-2xl font-bold text-gs-navy">{totals.value}</p>
|
| 222 |
+
)}
|
| 223 |
+
</div>
|
| 224 |
+
<div className="h-10 w-px bg-gray-100"></div>
|
| 225 |
+
<div>
|
| 226 |
+
<p className="text-[10px] font-bold text-gs-slate uppercase tracking-widest mb-1">Day Change</p>
|
| 227 |
+
{totals.loading ? (
|
| 228 |
+
<div className="h-8 w-24 bg-gray-100 animate-pulse rounded"></div>
|
| 229 |
+
) : (
|
| 230 |
+
<p className={`text-lg font-bold ${totals.isPositive ? 'text-green-600' : 'text-red-500'}`}>
|
| 231 |
+
{totals.isPositive ? '+' : ''}{totals.change}
|
| 232 |
+
<span className="text-xs ml-1 font-medium">({totals.isPositive ? '+' : ''}{totals.percent}%)</span>
|
| 233 |
+
</p>
|
| 234 |
+
)}
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</header>
|
| 238 |
+
|
| 239 |
+
{/* Top Section: Allocation */}
|
| 240 |
+
<div className="bg-white rounded-2xl p-8 shadow-sm border border-gray-100 flex flex-col md:flex-row items-center mb-8">
|
| 241 |
+
<div className="w-full md:w-1/2 h-64">
|
| 242 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 243 |
+
<PieChart>
|
| 244 |
+
<Pie
|
| 245 |
+
data={displayPortfolio.allocation}
|
| 246 |
+
cx="50%"
|
| 247 |
+
cy="50%"
|
| 248 |
+
innerRadius={80}
|
| 249 |
+
outerRadius={110}
|
| 250 |
+
paddingAngle={2}
|
| 251 |
+
dataKey="value"
|
| 252 |
+
nameKey="name"
|
| 253 |
+
stroke="none"
|
| 254 |
+
>
|
| 255 |
+
{displayPortfolio.allocation.map((entry, index) => (
|
| 256 |
+
<Cell key={`cell-${index}`} fill={entry.color} />
|
| 257 |
+
))}
|
| 258 |
+
</Pie>
|
| 259 |
+
<Tooltip
|
| 260 |
+
formatter={(value, name) => [`${value}%`, name]}
|
| 261 |
+
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' }}
|
| 262 |
+
/>
|
| 263 |
+
</PieChart>
|
| 264 |
+
</ResponsiveContainer>
|
| 265 |
+
</div>
|
| 266 |
+
<div className="w-full md:w-1/2 space-y-4">
|
| 267 |
+
<div className="flex justify-between items-center mb-4">
|
| 268 |
+
<h3 className="text-xl font-medium text-gs-navy">Current Allocation</h3>
|
| 269 |
+
<div className="flex gap-2">
|
| 270 |
+
<button
|
| 271 |
+
onClick={() => setIsSearchOpen(true)}
|
| 272 |
+
className="bg-gs-navy text-white p-1.5 rounded-lg hover:bg-gs-gold hover:text-gs-navy transition-all shadow-sm"
|
| 273 |
+
title="Add Asset"
|
| 274 |
+
>
|
| 275 |
+
<Plus size={18} />
|
| 276 |
+
</button>
|
| 277 |
+
<button
|
| 278 |
+
onClick={() => setShowHeatmap(true)}
|
| 279 |
+
className="text-gs-slate hover:text-gs-gold transition-colors p-1"
|
| 280 |
+
title="View Heatmap"
|
| 281 |
+
>
|
| 282 |
+
<LayoutGrid size={18} />
|
| 283 |
+
</button>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
|
| 287 |
+
{/* Asset Type Split Indicator */}
|
| 288 |
+
<div className="mb-6 bg-gs-light/30 p-4 rounded-xl border border-gray-100">
|
| 289 |
+
<div className="flex justify-between text-[10px] font-bold text-gs-slate uppercase tracking-widest mb-2">
|
| 290 |
+
<span>Stocks ({displayPortfolio.stockSplit.toFixed(2)}%)</span>
|
| 291 |
+
<span>Mutual Funds ({displayPortfolio.mfSplit.toFixed(2)}%)</span>
|
| 292 |
+
</div>
|
| 293 |
+
<div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden flex">
|
| 294 |
+
<div
|
| 295 |
+
className="h-full bg-gs-navy transition-all duration-1000"
|
| 296 |
+
style={{ width: `${displayPortfolio.stockSplit}%` }}
|
| 297 |
+
></div>
|
| 298 |
+
<div
|
| 299 |
+
className="h-full bg-gs-gold transition-all duration-1000"
|
| 300 |
+
style={{ width: `${displayPortfolio.mfSplit}%` }}
|
| 301 |
+
></div>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<p className="text-xs text-gs-slate mb-3 italic">Click an asset to view historical performance and AI analysis.</p>
|
| 306 |
+
<div className="max-h-60 overflow-y-auto pr-2">
|
| 307 |
+
{displayPortfolio.allocation.length > 0 ? (
|
| 308 |
+
displayPortfolio.allocation.map((asset, idx) => (
|
| 309 |
+
<button
|
| 310 |
+
key={idx}
|
| 311 |
+
onClick={() => {
|
| 312 |
+
if (asset.ticker) {
|
| 313 |
+
setActivePopupAsset({ ticker: asset.ticker, name: asset.name });
|
| 314 |
+
}
|
| 315 |
+
}}
|
| 316 |
+
className="w-full text-left flex justify-between items-center p-3 rounded-lg transition-colors mb-2 bg-gs-light/50 hover:bg-gray-100 hover:shadow-sm border border-transparent hover:border-gray-200 group"
|
| 317 |
+
>
|
| 318 |
+
<div className="flex items-center justify-between w-full">
|
| 319 |
+
<div className="flex items-center">
|
| 320 |
+
<div className="w-4 h-4 rounded-full mr-3 shadow-sm" style={{ backgroundColor: asset.color }}></div>
|
| 321 |
+
<div className="flex flex-col items-start">
|
| 322 |
+
<span className="text-gs-slate font-medium text-sm group-hover:text-gs-navy transition-colors">{asset.name}</span>
|
| 323 |
+
<div className="flex items-center gap-2 mt-0.5">
|
| 324 |
+
<span className="text-[10px] text-gray-400 font-medium">{asset.shares} shares</span>
|
| 325 |
+
{asset.dayChange !== undefined && (
|
| 326 |
+
<span className={`text-[10px] font-bold ${asset.dayChange >= 0 ? 'text-green-600' : 'text-red-500'}`}>
|
| 327 |
+
{asset.dayChange >= 0 ? '+' : ''}${Math.abs(asset.dollarChange || 0).toFixed(2)} ({asset.dayChange >= 0 ? '▲' : '▼'} {Math.abs(asset.dayChange).toFixed(2)}%)
|
| 328 |
+
</span>
|
| 329 |
+
)}
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
<span className="font-bold text-gs-navy text-sm ml-4">{asset.value.toFixed(2)}%</span>
|
| 334 |
+
</div>
|
| 335 |
+
</button>
|
| 336 |
+
))
|
| 337 |
+
) : (
|
| 338 |
+
<div className="text-center py-12 bg-gs-light/20 rounded-2xl border-2 border-dashed border-gray-200">
|
| 339 |
+
<p className="text-gs-slate text-sm font-light mb-6 italic">Your portfolio is currently empty.</p>
|
| 340 |
+
<button
|
| 341 |
+
onClick={() => setIsSearchOpen(true)}
|
| 342 |
+
className="inline-flex items-center gap-2 px-8 py-3 bg-gs-navy text-white rounded-xl hover:bg-gs-gold hover:text-gs-navy transition-all shadow-lg font-bold"
|
| 343 |
+
>
|
| 344 |
+
<Plus size={18} />
|
| 345 |
+
Build Your Portfolio
|
| 346 |
+
</button>
|
| 347 |
+
</div>
|
| 348 |
+
)}
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
|
| 353 |
+
{/* Investment & Retirement Planning Section */}
|
| 354 |
+
<FinancialCalculators />
|
| 355 |
+
|
| 356 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
|
| 357 |
+
{/* Left Column: Indicators */}
|
| 358 |
+
<div className="lg:col-span-2 space-y-8">
|
| 359 |
+
<Indicators portfolio={displayPortfolio} />
|
| 360 |
+
<FeeTransparencyModule portfolio={displayPortfolio} />
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
{/* Right Column: Rebalancing Engine */}
|
| 364 |
+
<div className="space-y-8">
|
| 365 |
+
<RebalancingEngine onScenarioSelect={handleRebalance} />
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
|
| 369 |
+
{/* Bottom Section: MacroTracker */}
|
| 370 |
+
<MacroTracker />
|
| 371 |
+
</div>
|
| 372 |
+
|
| 373 |
+
<TransparencyModal
|
| 374 |
+
isOpen={!!modalData}
|
| 375 |
+
onClose={closeModal}
|
| 376 |
+
data={modalData}
|
| 377 |
+
currentPortfolio={displayPortfolio}
|
| 378 |
+
totalValue={totals.rawValue}
|
| 379 |
+
prices={prices}
|
| 380 |
+
/>
|
| 381 |
+
|
| 382 |
+
<StockPopup
|
| 383 |
+
ticker={activePopupAsset?.ticker}
|
| 384 |
+
assetName={activePopupAsset?.name}
|
| 385 |
+
onClose={closeStockPopup}
|
| 386 |
+
/>
|
| 387 |
+
|
| 388 |
+
<AnimatePresence>
|
| 389 |
+
{showHeatmap && (
|
| 390 |
+
<PortfolioHeatmap
|
| 391 |
+
onClose={() => setShowHeatmap(false)}
|
| 392 |
+
allocation={displayPortfolio.allocation}
|
| 393 |
+
/>
|
| 394 |
+
)}
|
| 395 |
+
</AnimatePresence>
|
| 396 |
+
|
| 397 |
+
<AnimatePresence>
|
| 398 |
+
{isSearchOpen && (
|
| 399 |
+
<StockSearch
|
| 400 |
+
isOpen={isSearchOpen}
|
| 401 |
+
onClose={() => setIsSearchOpen(false)}
|
| 402 |
+
onAddStock={handleAddStock}
|
| 403 |
+
currentPortfolio={displayPortfolio}
|
| 404 |
+
totalValue={totals.value}
|
| 405 |
+
/>
|
| 406 |
+
)}
|
| 407 |
+
</AnimatePresence>
|
| 408 |
+
</div>
|
| 409 |
+
);
|
| 410 |
+
}
|
src/pages/LandingPage.jsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 4 |
+
import { ShieldCheck, TrendingUp, Zap } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
const questions = [
|
| 7 |
+
{
|
| 8 |
+
id: 1,
|
| 9 |
+
text: "What are you saving for?",
|
| 10 |
+
options: [
|
| 11 |
+
{ label: "A major purchase soon (house, car)", score: 1 },
|
| 12 |
+
{ label: "General wealth building", score: 2 },
|
| 13 |
+
{ label: "Retirement (20+ years away)", score: 3 }
|
| 14 |
+
]
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
id: 2,
|
| 18 |
+
text: "How do you feel about sudden market drops?",
|
| 19 |
+
options: [
|
| 20 |
+
{ label: "I'd panic and want to sell", score: 1 },
|
| 21 |
+
{ label: "I'd be concerned but hold on", score: 2 },
|
| 22 |
+
{ label: "I'd see it as a buying opportunity", score: 3 }
|
| 23 |
+
]
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
id: 3,
|
| 27 |
+
text: "What is your primary investment goal?",
|
| 28 |
+
options: [
|
| 29 |
+
{ label: "Protect my money from losing value", score: 1 },
|
| 30 |
+
{ label: "Steady, moderate growth over time", score: 2 },
|
| 31 |
+
{ label: "Maximum growth, regardless of ups and downs", score: 3 }
|
| 32 |
+
]
|
| 33 |
+
}
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
export default function LandingPage({ setRiskProfile }) {
|
| 37 |
+
const [currentStep, setCurrentStep] = useState(0);
|
| 38 |
+
const [totalScore, setTotalScore] = useState(0);
|
| 39 |
+
const navigate = useNavigate();
|
| 40 |
+
|
| 41 |
+
const handleOptionSelect = (score) => {
|
| 42 |
+
setTotalScore(prev => prev + score);
|
| 43 |
+
if (currentStep < questions.length - 1) {
|
| 44 |
+
setCurrentStep(prev => prev + 1);
|
| 45 |
+
} else {
|
| 46 |
+
determineProfile(totalScore + score);
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const determineProfile = (finalScore) => {
|
| 51 |
+
let profile = 'Balanced';
|
| 52 |
+
if (finalScore <= 4) profile = 'Cautious';
|
| 53 |
+
if (finalScore >= 8) profile = 'Bold';
|
| 54 |
+
|
| 55 |
+
setRiskProfile(profile);
|
| 56 |
+
navigate('/dashboard');
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
return (
|
| 60 |
+
<div className="min-h-screen bg-gs-navy flex flex-col items-center justify-center p-6 relative overflow-hidden">
|
| 61 |
+
{/* Decorative background elements */}
|
| 62 |
+
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-gs-gold/20 rounded-full blur-[120px]"></div>
|
| 63 |
+
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-blue-500/10 rounded-full blur-[150px]"></div>
|
| 64 |
+
|
| 65 |
+
<div className="z-10 text-center mb-12">
|
| 66 |
+
<h1 className="text-4xl md:text-5xl font-light text-white mb-4 tracking-tight">
|
| 67 |
+
Wealth management, <span className="text-gs-gold font-normal">simplified.</span>
|
| 68 |
+
</h1>
|
| 69 |
+
<p className="text-gs-light/70 text-lg md:text-xl max-w-xl mx-auto font-light">
|
| 70 |
+
No jargon. No hidden fees. Just clear strategies tailored to your goals. Let's find out what kind of investor you are.
|
| 71 |
+
</p>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div className="z-10 w-full max-w-2xl bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl p-8 md:p-12">
|
| 75 |
+
<div className="flex justify-between mb-8">
|
| 76 |
+
{questions.map((q, idx) => (
|
| 77 |
+
<div key={q.id} className="flex-1 flex items-center">
|
| 78 |
+
<div className={`h-2 w-full rounded-full transition-all duration-500 ${idx <= currentStep ? 'bg-gs-gold' : 'bg-white/20'}`}></div>
|
| 79 |
+
{idx < questions.length - 1 && <div className="w-4"></div>}
|
| 80 |
+
</div>
|
| 81 |
+
))}
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<AnimatePresence mode="wait">
|
| 85 |
+
<motion.div
|
| 86 |
+
key={currentStep}
|
| 87 |
+
initial={{ opacity: 0, x: 20 }}
|
| 88 |
+
animate={{ opacity: 1, x: 0 }}
|
| 89 |
+
exit={{ opacity: 0, x: -20 }}
|
| 90 |
+
transition={{ duration: 0.3 }}
|
| 91 |
+
>
|
| 92 |
+
<h2 className="text-2xl text-white font-medium mb-6 text-center">
|
| 93 |
+
{questions[currentStep].text}
|
| 94 |
+
</h2>
|
| 95 |
+
<div className="space-y-4">
|
| 96 |
+
{questions[currentStep].options.map((option, idx) => (
|
| 97 |
+
<button
|
| 98 |
+
key={idx}
|
| 99 |
+
onClick={() => handleOptionSelect(option.score)}
|
| 100 |
+
className="w-full text-left px-6 py-4 rounded-xl bg-white/5 border border-white/10 hover:bg-gs-gold/20 hover:border-gs-gold/50 transition-all duration-300 text-white font-light flex items-center group"
|
| 101 |
+
>
|
| 102 |
+
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center mr-4 group-hover:bg-gs-gold/30 group-hover:text-gs-gold transition-colors">
|
| 103 |
+
{idx === 0 && <ShieldCheck size={18} />}
|
| 104 |
+
{idx === 1 && <TrendingUp size={18} />}
|
| 105 |
+
{idx === 2 && <Zap size={18} />}
|
| 106 |
+
</div>
|
| 107 |
+
{option.label}
|
| 108 |
+
</button>
|
| 109 |
+
))}
|
| 110 |
+
</div>
|
| 111 |
+
</motion.div>
|
| 112 |
+
</AnimatePresence>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
);
|
| 116 |
+
}
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
'gs-navy': '#0B233F',
|
| 11 |
+
'gs-slate': '#2C3E50',
|
| 12 |
+
'gs-gold': '#C5A880',
|
| 13 |
+
'gs-light': '#F8F9FA',
|
| 14 |
+
},
|
| 15 |
+
fontFamily: {
|
| 16 |
+
sans: ['Inter', 'sans-serif'],
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
plugins: [],
|
| 21 |
+
}
|
vite.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vite.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
server: {
|
| 8 |
+
proxy: {
|
| 9 |
+
'/api/yahoo': {
|
| 10 |
+
target: 'https://query1.finance.yahoo.com',
|
| 11 |
+
changeOrigin: true,
|
| 12 |
+
rewrite: (path) => path.replace(/^\/api\/yahoo/, '')
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
})
|