Commit ef26a94e authored by Anthony Jacob's avatar Anthony Jacob
Browse files

finalize POC and first CICD

parent 02c9e4c6
Loading
Loading
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"]
+57 −10
Original line number Diff line number Diff line
@@ -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');
@@ -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>
+73 −11
Original line number Diff line number Diff line
@@ -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);
@@ -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>
@@ -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">
@@ -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() : '-'}