Loading .dockerignore 0 → 100644 +9 −0 Original line number Diff line number Diff line node_modules .next .git .env .env.* *.log Dockerfile .dockerignore README.md .gitlab-ci.yml 0 → 100644 +57 −0 Original line number Diff line number Diff line # For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages # predefined variables https://docs.gitlab.com/ee/ci/variables/predefined_variables.html stages: - publish - deploy publish: stage: publish image: docker:cli services: - docker:dind variables: DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest before_script: - echo "CI_REGISTRY_IMAGE $CI_REGISTRY_IMAGE" - echo "DOCKER_IMAGE_NAME $DOCKER_IMAGE_NAME" - echo "CI_REGISTRY $CI_REGISTRY" - echo "DOCKER_HOST $DOCKER_HOST" - echo "CI_COMMIT_BRANCH $CI_COMMIT_BRANCH" - echo "CI_DEFAULT_BRANCH $CI_DEFAULT_BRANCH" - docker info - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY rules: - if: $CI_COMMIT_BRANCH == "main" exists: - Dockerfile environment: Development script: - echo "Build Docker image..." - docker build -t $DOCKER_IMAGE_NAME . -f Dockerfile - echo "Publish Docker image..." - docker push $DOCKER_IMAGE_NAME - echo "image successfully published." deploy-demo: stage: deploy image: ubuntu:24.04 variables: DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest before_script: - apt-get -yq update - apt-get -yqq install ssh - install -m 600 -D /dev/null ~/.ssh/id_rsa - echo "$SSH_KEY" | base64 -d > ~/.ssh/id_rsa - ssh-keyscan -p $SSH_PORT -H $SSH_HOST > ~/.ssh/known_hosts - cat ~/.ssh/known_hosts script: - ssh $SSH_USER@$SSH_HOST -p $SSH_PORT "docker service update --force --image $DOCKER_IMAGE_NAME stack_tradingbot_front" after_script: - rm -rf ~/.ssh rules: - if: $CI_COMMIT_BRANCH == "main" environment: Development Dockerfile 0 → 100644 +25 −0 Original line number Diff line number Diff line # syntax=docker/dockerfile:1 FROM node:20-alpine AS base WORKDIR /app FROM base AS deps COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner ENV NODE_ENV=production ENV PORT=3000 COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public COPY --from=builder /app/node_modules ./node_modules EXPOSE 3000 CMD ["npm", "run", "start"] src/app/app/portfolios/view/[id]/chat/portfolioViewChatHistory.tsx +57 −10 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ export default function PortfolioViewChatHistoryClient(props: { const [to, setTo] = useState(10); const [isLoading, setIsLoading] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [openContent, setOpenContent] = useState<Record<number, boolean>>({}); const [openInstructions, setOpenInstructions] = useState<Record<number, boolean>>({}); const ticker = useMemo(() => { const value = searchParams.get('ticker'); Loading Loading @@ -112,24 +114,69 @@ export default function PortfolioViewChatHistoryClient(props: { {items.map((item) => ( <div key={item.id} className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="flex flex-wrap items-center justify-between gap-2 text-sm text-gray-500"> <div>{item.createdAt ? new Date(item.createdAt).toLocaleString() : ''}</div> <div> <span className="font-semibold text-gray-700">{item.role}</span> {item?.session?.purpose ? ` · ${item.session.purpose}` : ''} </div> <div>{item.createdAt ? new Date(item.createdAt).toLocaleString() : ''}</div> </div> {item.latencyMs && ( <div className="flex items-center gap-1"> <span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-semibold text-blue-700"> <i className="fa-solid fa-clock mr-2"></i> {(item.latencyMs / 1000).toFixed(2)} s </span> </div> )} <div className="flex items-center gap-1"> {item.isInstructionSuccess ? ( <span className="rounded-full bg-green-100 px-2 py-1 text-xs font-semibold text-green-700"> <i className="fa-solid fa-check mr-2"></i>Success</span> ) : ( <span className="rounded-full bg-red-100 px-2 py-1 text-xs font-semibold text-red-700"> <i className="fa-solid fa-xmark mr-2"></i>Failed</span> )} </div> </div> {/* Collapsible Content Section */} <div className="mt-3"> <button onClick={() => { setOpenContent((prev) => ({ ...prev, [item.id]: !prev[item.id], })); }} className="text-sm font-semibold text-sky-500 hover:underline mb-2 rounded-full bg-blue-500 px-3 py-1 text-sm leading-5 font-semibold text-white hover:bg-blue-700 items-center gap-2 cursor-pointer" > <i className={`fa-solid ${openContent[item.id] ? 'fa-eye-slash' : 'fa-eye'} mr-2`}></i> Details </button> <div id={`content-${item.id}`} className={openContent[item.id] ? "" : "hidden"}> <div className="text-sm font-semibold text-gray-700 mb-1">Content</div> <div className="whitespace-pre-wrap text-gray-800 leading-relaxed">{item.content}</div> </div> {item.instruction && ( <div className="mt-4"> <div className="text-sm font-semibold text-gray-700 mb-1">Instruction</div> <pre className="whitespace-pre-wrap rounded-md bg-gray-50 p-3 text-sm text-gray-700">{item.instruction}</pre> </div> )} <div className="mt-3"> <button onClick={() => { setOpenInstructions((prev) => ({ ...prev, [item.id]: !prev[item.id], })); }} className="text-sm font-semibold text-sky-500 hover:underline mb-2 rounded-full bg-blue-500 px-3 py-1 text-sm leading-5 font-semibold text-white hover:bg-blue-700 items-center gap-2 cursor-pointer" > <i className={`fa-solid ${openInstructions[item.id] ? 'fa-eye-slash' : 'fa-eye'} mr-2`}></i> Instruction Log </button> <div id={`transaction-log-${item.id}`} className={openInstructions[item.id] ? "" : "hidden"}> <div className="text-sm font-semibold text-gray-700 mb-1">Instruction Log</div> <div className="whitespace-pre-wrap text-gray-800 leading-relaxed">{item.instructionLog}</div> </div> </div> </div> ))} </div> Loading src/app/app/portfolios/view/[id]/portfolioViewDetailClient.tsx +73 −11 Original line number Diff line number Diff line Loading @@ -24,6 +24,10 @@ export default function PortfolioViewDetailClient(props: { const getPositionTicker = (position: any) => position.ticker || "N/A"; const getPositionQuantity = (position: any) => Number(position.quantity); const formatPrice = (value: any) => Number(value ?? 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, }); const getEntryPrice = (position: any) => { const qty = getPositionQuantity(position); const unitPrice = Number(position.buyUnitPrice); Loading Loading @@ -171,27 +175,85 @@ export default function PortfolioViewDetailClient(props: { <div className="text-center text-gray-500">Portfolio not found.</div> ) : ( <> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <h2 className="text-xl font-semibold mb-4">{portfolio.name}</h2> </div> <div className="grid gap-4 md:grid-cols-3"> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Initial Investment</div> <div className="text-2xl font-semibold text-gray-900">{Number(portfolio.initialCash ?? 0).toLocaleString()}</div> <div className="text-2xl font-semibold text-gray-900">{formatPrice(portfolio.initialCash)}</div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Cash Available</div> <div className="text-2xl font-semibold text-gray-900">{Number(portfolio.currentCash ?? 0).toLocaleString()}</div> <div className="text-2xl font-semibold text-gray-900">{formatPrice(portfolio.currentCash)}</div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Active Portfolio Value</div> <div className="text-2xl font-semibold text-gray-900">{Number(activePortfolioValue).toLocaleString()}</div> <div className="text-2xl font-semibold text-gray-900">{formatPrice(activePortfolioValue)}</div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Profit / losses</div> <div className={`text-2xl font-semibold text-gray-900 ${profitLosses >= 0 ? 'text-green-600' : 'text-red-600'}`}>{Number(profitLosses).toLocaleString()} / {profitLossesPercent.toFixed(2)}%</div> <div className={`text-2xl font-semibold text-gray-900 ${profitLosses >= 0 ? 'text-green-600' : 'text-red-600'}`}>{formatPrice(profitLosses)} / {formatPrice(profitLossesPercent)}%</div> </div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className=""> {portfolio.rateLimit && portfolio.rateRemaining && portfolio.usageLimit && portfolio.usageRemaining && ( <div className="mb-4 rounded-md bg-blue-50 p-4"> <div className="flex"> <div className="flex-shrink-0"> <i className="fa-solid fa-triangle-exclamation text-blue-400"></i> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-blue-800">Stock Data Info</h3> Rate limit Remaining: {portfolio.rateRemaining} / {portfolio.rateLimit} <br /> Usage limit Remaining: {portfolio.usageRemaining} / {portfolio.usageLimit} </div> </div> </div> )} {portfolio.stockDataWarnings && portfolio.stockDataWarnings.length > 0 && ( <div className="mb-4 rounded-md bg-yellow-50 p-4"> <div className="flex"> <div className="flex-shrink-0"> <i className="fa-solid fa-triangle-exclamation text-yellow-400"></i> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-yellow-800">Stock Data Warning</h3> <ul className="mt-2 text-sm text-yellow-700 list-disc list-inside"> {portfolio.stockDataWarnings.map((warning: string, index: number) => ( <li key={index}>{warning}</li> ))} </ul> </div> </div> </div> )} {portfolio.stockDataErrors && portfolio.stockDataErrors.length > 0 && ( <div className="mb-4 rounded-md bg-red-50 p-4"> <div className="flex"> <div className="flex-shrink-0"> <i className="fa-solid fa-triangle-exclamation text-red-400"></i> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-red-800">Stock Data Error</h3> <ul className="mt-2 text-sm text-red-700 list-disc list-inside"> {portfolio.stockDataErrors.map((errors: { code: string, message: string }, index: number) => ( <li key={index}>{errors.code} {errors.message}</li> ))} </ul> </div> </div> </div> )} </div> <h2 className="text-xl font-semibold mb-4">Positions by Ticker</h2> {groupedPositionsList.length === 0 ? ( <div className="text-gray-500">No open positions found.</div> Loading @@ -216,11 +278,11 @@ export default function PortfolioViewDetailClient(props: { <tr key={group.ticker}> <td className="border border-gray-200 px-4 py-2">{`${group.name} (${group.ticker})`}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(group.quantity).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(avgEntry).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(group.lastPrice).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(group.currentValue).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(avgEntry)}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(group.lastPrice)}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(group.currentValue)}</td> <td className={`border border-gray-200 px-4 py-2 text-right ${group.pnl >= 0 ? 'text-green-600' : 'text-red-600'}`}> {Number(group.pnl).toLocaleString()} {formatPrice(group.pnl)} </td> <td className="border border-gray-200 px-4 py-2 text-right"> Loading Loading @@ -303,10 +365,10 @@ export default function PortfolioViewDetailClient(props: { <tr key={position?.id ?? `${getPositionTicker(position)}-${closedAt || Math.random()}`}> <td className="border border-gray-200 px-4 py-2">{getPositionTicker(position)}</td> <td className="border border-gray-200 px-4 py-2 text-right">{getPositionQuantity(position).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(getEntryPrice(position)).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(position?.sellUnitPrice ?? position?.exitPrice ?? position?.closePrice ?? position?.sellPrice ?? 0).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(getEntryPrice(position))}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(position?.sellUnitPrice ?? position?.exitPrice ?? position?.closePrice ?? position?.sellPrice ?? 0)}</td> <td className={`border border-gray-200 px-4 py-2 text-right ${pnl >= 0 ? 'text-green-600' : 'text-red-600'}`}> {pnl.toLocaleString()} {formatPrice(pnl)} </td> <td className="border border-gray-200 px-4 py-2 text-right"> {closedAt ? new Date(closedAt).toLocaleString() : '-'} Loading Loading
.dockerignore 0 → 100644 +9 −0 Original line number Diff line number Diff line node_modules .next .git .env .env.* *.log Dockerfile .dockerignore README.md
.gitlab-ci.yml 0 → 100644 +57 −0 Original line number Diff line number Diff line # For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages # predefined variables https://docs.gitlab.com/ee/ci/variables/predefined_variables.html stages: - publish - deploy publish: stage: publish image: docker:cli services: - docker:dind variables: DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest before_script: - echo "CI_REGISTRY_IMAGE $CI_REGISTRY_IMAGE" - echo "DOCKER_IMAGE_NAME $DOCKER_IMAGE_NAME" - echo "CI_REGISTRY $CI_REGISTRY" - echo "DOCKER_HOST $DOCKER_HOST" - echo "CI_COMMIT_BRANCH $CI_COMMIT_BRANCH" - echo "CI_DEFAULT_BRANCH $CI_DEFAULT_BRANCH" - docker info - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY rules: - if: $CI_COMMIT_BRANCH == "main" exists: - Dockerfile environment: Development script: - echo "Build Docker image..." - docker build -t $DOCKER_IMAGE_NAME . -f Dockerfile - echo "Publish Docker image..." - docker push $DOCKER_IMAGE_NAME - echo "image successfully published." deploy-demo: stage: deploy image: ubuntu:24.04 variables: DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest before_script: - apt-get -yq update - apt-get -yqq install ssh - install -m 600 -D /dev/null ~/.ssh/id_rsa - echo "$SSH_KEY" | base64 -d > ~/.ssh/id_rsa - ssh-keyscan -p $SSH_PORT -H $SSH_HOST > ~/.ssh/known_hosts - cat ~/.ssh/known_hosts script: - ssh $SSH_USER@$SSH_HOST -p $SSH_PORT "docker service update --force --image $DOCKER_IMAGE_NAME stack_tradingbot_front" after_script: - rm -rf ~/.ssh rules: - if: $CI_COMMIT_BRANCH == "main" environment: Development
Dockerfile 0 → 100644 +25 −0 Original line number Diff line number Diff line # syntax=docker/dockerfile:1 FROM node:20-alpine AS base WORKDIR /app FROM base AS deps COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner ENV NODE_ENV=production ENV PORT=3000 COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public COPY --from=builder /app/node_modules ./node_modules EXPOSE 3000 CMD ["npm", "run", "start"]
src/app/app/portfolios/view/[id]/chat/portfolioViewChatHistory.tsx +57 −10 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ export default function PortfolioViewChatHistoryClient(props: { const [to, setTo] = useState(10); const [isLoading, setIsLoading] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [openContent, setOpenContent] = useState<Record<number, boolean>>({}); const [openInstructions, setOpenInstructions] = useState<Record<number, boolean>>({}); const ticker = useMemo(() => { const value = searchParams.get('ticker'); Loading Loading @@ -112,24 +114,69 @@ export default function PortfolioViewChatHistoryClient(props: { {items.map((item) => ( <div key={item.id} className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="flex flex-wrap items-center justify-between gap-2 text-sm text-gray-500"> <div>{item.createdAt ? new Date(item.createdAt).toLocaleString() : ''}</div> <div> <span className="font-semibold text-gray-700">{item.role}</span> {item?.session?.purpose ? ` · ${item.session.purpose}` : ''} </div> <div>{item.createdAt ? new Date(item.createdAt).toLocaleString() : ''}</div> </div> {item.latencyMs && ( <div className="flex items-center gap-1"> <span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-semibold text-blue-700"> <i className="fa-solid fa-clock mr-2"></i> {(item.latencyMs / 1000).toFixed(2)} s </span> </div> )} <div className="flex items-center gap-1"> {item.isInstructionSuccess ? ( <span className="rounded-full bg-green-100 px-2 py-1 text-xs font-semibold text-green-700"> <i className="fa-solid fa-check mr-2"></i>Success</span> ) : ( <span className="rounded-full bg-red-100 px-2 py-1 text-xs font-semibold text-red-700"> <i className="fa-solid fa-xmark mr-2"></i>Failed</span> )} </div> </div> {/* Collapsible Content Section */} <div className="mt-3"> <button onClick={() => { setOpenContent((prev) => ({ ...prev, [item.id]: !prev[item.id], })); }} className="text-sm font-semibold text-sky-500 hover:underline mb-2 rounded-full bg-blue-500 px-3 py-1 text-sm leading-5 font-semibold text-white hover:bg-blue-700 items-center gap-2 cursor-pointer" > <i className={`fa-solid ${openContent[item.id] ? 'fa-eye-slash' : 'fa-eye'} mr-2`}></i> Details </button> <div id={`content-${item.id}`} className={openContent[item.id] ? "" : "hidden"}> <div className="text-sm font-semibold text-gray-700 mb-1">Content</div> <div className="whitespace-pre-wrap text-gray-800 leading-relaxed">{item.content}</div> </div> {item.instruction && ( <div className="mt-4"> <div className="text-sm font-semibold text-gray-700 mb-1">Instruction</div> <pre className="whitespace-pre-wrap rounded-md bg-gray-50 p-3 text-sm text-gray-700">{item.instruction}</pre> </div> )} <div className="mt-3"> <button onClick={() => { setOpenInstructions((prev) => ({ ...prev, [item.id]: !prev[item.id], })); }} className="text-sm font-semibold text-sky-500 hover:underline mb-2 rounded-full bg-blue-500 px-3 py-1 text-sm leading-5 font-semibold text-white hover:bg-blue-700 items-center gap-2 cursor-pointer" > <i className={`fa-solid ${openInstructions[item.id] ? 'fa-eye-slash' : 'fa-eye'} mr-2`}></i> Instruction Log </button> <div id={`transaction-log-${item.id}`} className={openInstructions[item.id] ? "" : "hidden"}> <div className="text-sm font-semibold text-gray-700 mb-1">Instruction Log</div> <div className="whitespace-pre-wrap text-gray-800 leading-relaxed">{item.instructionLog}</div> </div> </div> </div> ))} </div> Loading
src/app/app/portfolios/view/[id]/portfolioViewDetailClient.tsx +73 −11 Original line number Diff line number Diff line Loading @@ -24,6 +24,10 @@ export default function PortfolioViewDetailClient(props: { const getPositionTicker = (position: any) => position.ticker || "N/A"; const getPositionQuantity = (position: any) => Number(position.quantity); const formatPrice = (value: any) => Number(value ?? 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, }); const getEntryPrice = (position: any) => { const qty = getPositionQuantity(position); const unitPrice = Number(position.buyUnitPrice); Loading Loading @@ -171,27 +175,85 @@ export default function PortfolioViewDetailClient(props: { <div className="text-center text-gray-500">Portfolio not found.</div> ) : ( <> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <h2 className="text-xl font-semibold mb-4">{portfolio.name}</h2> </div> <div className="grid gap-4 md:grid-cols-3"> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Initial Investment</div> <div className="text-2xl font-semibold text-gray-900">{Number(portfolio.initialCash ?? 0).toLocaleString()}</div> <div className="text-2xl font-semibold text-gray-900">{formatPrice(portfolio.initialCash)}</div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Cash Available</div> <div className="text-2xl font-semibold text-gray-900">{Number(portfolio.currentCash ?? 0).toLocaleString()}</div> <div className="text-2xl font-semibold text-gray-900">{formatPrice(portfolio.currentCash)}</div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Active Portfolio Value</div> <div className="text-2xl font-semibold text-gray-900">{Number(activePortfolioValue).toLocaleString()}</div> <div className="text-2xl font-semibold text-gray-900">{formatPrice(activePortfolioValue)}</div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className="text-sm text-gray-500">Profit / losses</div> <div className={`text-2xl font-semibold text-gray-900 ${profitLosses >= 0 ? 'text-green-600' : 'text-red-600'}`}>{Number(profitLosses).toLocaleString()} / {profitLossesPercent.toFixed(2)}%</div> <div className={`text-2xl font-semibold text-gray-900 ${profitLosses >= 0 ? 'text-green-600' : 'text-red-600'}`}>{formatPrice(profitLosses)} / {formatPrice(profitLossesPercent)}%</div> </div> </div> <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> <div className=""> {portfolio.rateLimit && portfolio.rateRemaining && portfolio.usageLimit && portfolio.usageRemaining && ( <div className="mb-4 rounded-md bg-blue-50 p-4"> <div className="flex"> <div className="flex-shrink-0"> <i className="fa-solid fa-triangle-exclamation text-blue-400"></i> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-blue-800">Stock Data Info</h3> Rate limit Remaining: {portfolio.rateRemaining} / {portfolio.rateLimit} <br /> Usage limit Remaining: {portfolio.usageRemaining} / {portfolio.usageLimit} </div> </div> </div> )} {portfolio.stockDataWarnings && portfolio.stockDataWarnings.length > 0 && ( <div className="mb-4 rounded-md bg-yellow-50 p-4"> <div className="flex"> <div className="flex-shrink-0"> <i className="fa-solid fa-triangle-exclamation text-yellow-400"></i> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-yellow-800">Stock Data Warning</h3> <ul className="mt-2 text-sm text-yellow-700 list-disc list-inside"> {portfolio.stockDataWarnings.map((warning: string, index: number) => ( <li key={index}>{warning}</li> ))} </ul> </div> </div> </div> )} {portfolio.stockDataErrors && portfolio.stockDataErrors.length > 0 && ( <div className="mb-4 rounded-md bg-red-50 p-4"> <div className="flex"> <div className="flex-shrink-0"> <i className="fa-solid fa-triangle-exclamation text-red-400"></i> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-red-800">Stock Data Error</h3> <ul className="mt-2 text-sm text-red-700 list-disc list-inside"> {portfolio.stockDataErrors.map((errors: { code: string, message: string }, index: number) => ( <li key={index}>{errors.code} {errors.message}</li> ))} </ul> </div> </div> </div> )} </div> <h2 className="text-xl font-semibold mb-4">Positions by Ticker</h2> {groupedPositionsList.length === 0 ? ( <div className="text-gray-500">No open positions found.</div> Loading @@ -216,11 +278,11 @@ export default function PortfolioViewDetailClient(props: { <tr key={group.ticker}> <td className="border border-gray-200 px-4 py-2">{`${group.name} (${group.ticker})`}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(group.quantity).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(avgEntry).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(group.lastPrice).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(group.currentValue).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(avgEntry)}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(group.lastPrice)}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(group.currentValue)}</td> <td className={`border border-gray-200 px-4 py-2 text-right ${group.pnl >= 0 ? 'text-green-600' : 'text-red-600'}`}> {Number(group.pnl).toLocaleString()} {formatPrice(group.pnl)} </td> <td className="border border-gray-200 px-4 py-2 text-right"> Loading Loading @@ -303,10 +365,10 @@ export default function PortfolioViewDetailClient(props: { <tr key={position?.id ?? `${getPositionTicker(position)}-${closedAt || Math.random()}`}> <td className="border border-gray-200 px-4 py-2">{getPositionTicker(position)}</td> <td className="border border-gray-200 px-4 py-2 text-right">{getPositionQuantity(position).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(getEntryPrice(position)).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{Number(position?.sellUnitPrice ?? position?.exitPrice ?? position?.closePrice ?? position?.sellPrice ?? 0).toLocaleString()}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(getEntryPrice(position))}</td> <td className="border border-gray-200 px-4 py-2 text-right">{formatPrice(position?.sellUnitPrice ?? position?.exitPrice ?? position?.closePrice ?? position?.sellPrice ?? 0)}</td> <td className={`border border-gray-200 px-4 py-2 text-right ${pnl >= 0 ? 'text-green-600' : 'text-red-600'}`}> {pnl.toLocaleString()} {formatPrice(pnl)} </td> <td className="border border-gray-200 px-4 py-2 text-right"> {closedAt ? new Date(closedAt).toLocaleString() : '-'} Loading