Skip to content
Sumit Parmar
CASE STUDY 022025 – 2026checking…

Nivesh-Path

AI behavioral-finance paper-trading platform

New investors repeat the same mistakes (selling in a panic, buying after a spike, putting everything in one stock) and rarely see it in their own trades.

Designed and built solo

Paper trading with virtual cash. Educational, not financial advice.

01 / The problem

What needed solving

Nivesh-Path ("path to investment") gives every user $100k in virtual cash to paper-trade real US stocks at live prices, then holds up a mirror: every trade is checked for panic selling, FOMO buying and overconcentration, using the prices at that moment.

An AI coach explains the habit with the user's own trades, and an advisor explains the portfolio's risk. Both are educational, not financial advice.

Real market data has a hard budget: the free plan allows 60 calls a minute, and a single trade needs about 11 price lookups. So the system has to be careful about money, about data and about cost at the same time.

60 calls a minute

Finnhub free-plan call limitFinnhub free plansource ↗

02 / Constraints

What the design had to respect

  • Money can't go negative

    Even with parallel orders from two app instances and retried requests, an account must never overdraw or buy twice.

  • A free-tier data budget

    Market data is rate-limited, so almost every lookup must be answered from a cache.

  • The LLM can fail

    No key, an outage, a rate limit or a refusal must still leave a working page; the model may never do the arithmetic.

  • Live users during changes

    Moving the ledger from MongoDB to PostgreSQL had to happen on the live site without losing a cent.

  • Cheap to run

    Always-on on DigitalOcean with managed databases; AWS only for a demo stack that is created, measured and destroyed.

03 / Architecture

How it fits together

Every box is a real part of the system. Select one to see what it does and why it was built that way.

The architecture as text

Parts

  • React SPA (React 18 · TypeScript): Markets, stock pages, the trade panel, the portfolio dashboard, the Behavioral Mirror and the AI Advisor. TanStack Query for server state, Zustand for UI state.
  • Express API (Node.js 22 · JWT): The REST API for auth, market data, orders, portfolio and patterns, with JWT on every private route and rate limits on the paid AI endpoints.
  • Quote cache (Redis TTL cache): A small TTL cache in front of every market-data lookup: quotes, profiles, key stats and news.
  • Finnhub (market data API): Live quotes, company profiles, metrics and news for US tickers.
  • Nightly job (EventBridge · Lambda): Every weekday at 4:30 pm New York, saves the daily close of every held stock and re-checks overconcentration for all users.
  • Behavioral Mirror (pure rule functions): Checks every trade for panic selling, FOMO buying and overconcentration using the prices at that moment, and scores impulse control and diversification.
  • AI service (FastAPI · LangChain): The Portfolio Advisor and the behavior coach. Computes the metrics in Python, retrieves the user's relevant trades, and asks the model for a schema-validated answer.
  • Claude API (Anthropic Claude API): The LLM provider: structured outputs validated against a Pydantic schema.
  • PostgreSQL ledger (Prisma · Neon): The money: accounts (NUMERIC cash) and trades. One database transaction per order.
  • MongoDB (users · AI memory): Users, Behavioral Mirror events and saved analyses. The source of truth the AI memory is rebuilt from.
  • ChromaDB (per-user RAG index): One vector collection per user holding their embedded trades, so the advisor can retrieve the most relevant history.
  • Grafana Cloud (OpenTelemetry): Traces, metrics and logs from both services, a dashboard and email alerts.

Connections

  • React SPA → Express API: REST with a JWT; orders carry an Idempotency-Key
  • Express API → Quote cache: every price lookup
  • Quote cache → Finnhub: only on a cache miss
  • Express API → PostgreSQL ledger: one transaction per order
  • Express API → Behavioral Mirror: check the trade
  • Behavioral Mirror → MongoDB: save the pattern
  • Express API ↔ AI service: advisor and coach requests; AI reads trades through /internal
  • AI service → ChromaDB: embed and retrieve trades
  • AI service → Claude API: prompt with computed metrics, structured answer
  • AI service → MongoDB: cold-start rebuild, saved insights
  • Nightly job → Express API: /internal jobs with a token
  • AI service → Grafana Cloud: traces, metrics, logs (both services)

04 / Follow one request

Follow one trade

A user buys a stock they've watched climb all week. Here is the path of that order, and of the coaching that follows.
  1. STEP 01

    The order is placed

    The trade panel posts the order with the user's JWT and a fresh Idempotency-Key. If the network drops and the browser retries, the same key returns the first result instead of buying twice.

  2. STEP 02

    The server prices it

    The price comes from the server, never the browser. The quote cache answers almost every lookup; only a miss reaches the market-data API.

  3. STEP 03

    One transaction moves the money

    In one PostgreSQL transaction the account row is locked (SELECT … FOR UPDATE), cash is checked and debited, and the trade is inserted. CHECK constraints refuse negative cash even if the code were wrong.

  4. STEP 04

    The Behavioral Mirror looks at it

    Was it bought after a real run-up, close to the 52-week high? Is one stock now too much of the account? A detected pattern is saved and shown in the trade panel right away.

  5. STEP 05

    The AI remembers the trade

    The trade is embedded into the user's own vector collection, so later questions can retrieve it. If that collection was wiped by a deploy, it's rebuilt from MongoDB.

  6. STEP 06

    The coach explains the habit

    Asked for coaching, the AI service computes the numbers in Python, retrieves the relevant trades, and asks the model for a schema-validated answer that quotes the user's own trades. No model? A rule-based answer instead.

  7. STEP 07

    At 4:30 pm New York, the nightly job runs

    EventBridge Scheduler starts a Step Functions workflow; a Lambda saves each held stock's close through the web service's token-only /internal API and re-checks concentration for everyone.

  8. STEP 08

    And it's all one trace

    OpenTelemetry carries the trace from Node into Python, so the whole request shows up as one trace in Grafana, with alerts on latency, errors and AI health.

05 / Key decisions

The choices, and what they cost

  1. DECISION 01

    Move the money to PostgreSQL

    CHOSE
    a PostgreSQL ledger (Prisma, NUMERIC money, CHECK constraints, SELECT … FOR UPDATE on the account row, idempotency keys)
    OVER
    keeping cash and trades in MongoDB with atomic $inc updates
    BECAUSE
    the database itself then refuses an overdraft, and row locks make parallel orders safe: 20 parallel $10,000 buys on a $100k account fill exactly ten.
    TRADE-OFF
    two databases to run, and a live cutover to plan: users, Mirror events and AI memory stay in MongoDB.
    decision record ↗ for Move the money to PostgreSQL
  2. DECISION 02

    Cut over with a short pause, not dual writes

    CHOSE
    a maintenance pause of about five minutes, a dry run, copy and verify tool, then one switch
    OVER
    dual-writing to both databases during the move
    BECAUSE
    it's simple and verifiable for a site at this scale; the verify step proved every user's cash and holdings matched before the switch.
    TRADE-OFF
    trading is paused briefly; rollback means switching back and exporting anything written since.
    decision record ↗ for Cut over with a short pause, not dual writes
  3. DECISION 03

    The model explains; code computes

    CHOSE
    computing P/L, weights and concentration in Python, sending them to the model, and overwriting the model's own diversification number with the computed one
    OVER
    letting the LLM read the holdings and do the maths
    BECAUSE
    an LLM's arithmetic isn't reliable, and live testing had already found two different diversification scores on two pages.
    TRADE-OFF
    more code in the AI service and a schema the model must follow; a rule-based fallback answers when the model can't.
    decision record ↗ for The model explains; code computes
  4. DECISION 04

    No NAT gateway on the AWS demo

    CHOSE
    tasks in public subnets with security groups as the inbound boundary, and the database in private subnets with no internet route
    OVER
    private subnets behind a NAT gateway
    BECAUSE
    a NAT gateway costs about $32/month even when idle, for a stack that exists a few hours per demo.
    TRADE-OFF
    no network-level egress control on the tasks; in production I'd use private subnets with a NAT or VPC endpoints.
    decision record ↗ for No NAT gateway on the AWS demo
  5. DECISION 05

    GitHub OIDC, no AWS keys

    CHOSE
    short-lived credentials from GitHub's OIDC token, with a deploy role fenced by region, service, name pattern and a permissions boundary
    OVER
    long-lived access keys stored as repository secrets
    BECAUSE
    there is no key to leak; even a tampered workflow can't create a role that does more than the boundary allows.
    TRADE-OFF
    a one-time bootstrap in CloudShell and more IAM policy to read and review.
    decision record ↗ for GitHub OIDC, no AWS keys

06 / The hardest bug

Symptom to regression test

The Mirror accused me of FOMO for an ordinary buy

  1. SYMPTOM

    Testing on my own account, the Behavioral Mirror flagged index-fund (SPY) buys as FOMO, 1.0% from the 52-week high after a +1.7% move, and it would flag a sale as a panic sell even when it was taken at a profit. The impulse-control score, and the coach's advice, were built on those false flags.

  2. HOW IT WAS FOUND

    In live testing on my own account. Each flag that looked wrong was traced to the rule in code, and a written review confirmed each one before anything changed.

  3. ROOT CAUSE

    The FOMO rule only asked 'is the price near its 52-week high?' and treated the run-up as a severity bonus, not a requirement. The panic rule never compared the sale price with the average cost, and only daily data was used, so a crash on the day itself was invisible.

  4. FIX

    Rule v2, as pure functions: no FOMO flag without a run-up of at least 5%; no panic flag at or above average cost; today's move and intraday high included. A start-up migration re-graded old events and deleted the false alarms, safely repeatable.

  5. WHAT GUARDS IT NOW

    Boundary tests at every threshold (run-ups of 4.99, 5, 10 and 15%; a sale at cost versus one cent below). Each fix was broken on purpose to prove its tests fail. Tests went from 238 to 278.

the full write-up ↗

07 / Results

Measured, with the file that proves each number

Each number holds only for the scope written under it.

CI on the main branch, live from GitHub:Nivesh-Path CI status

08 / What I'd do differently

With hindsight

  • Design the ledger in PostgreSQL from day one. Moving money between databases on a live site worked, but it cost a whole phase of tooling that a first schema would have avoided.
  • Measure capacity, not just latency. The load tests are closed-loop with think time, so they show response times, not the maximum throughput; I'd add an open-model arrival-rate test on dedicated hardware.
  • Write the Behavioral Mirror's thresholds as testable rules before shipping the first version, rather than finding the edge cases in live use.