Skip to content

Conversation

@prshntrajput
Copy link

@prshntrajput prshntrajput commented Dec 8, 2025

Overview

This PR adds a historical performance chart to the Investment Detail page. Users can now see how their basket investment has performed over time and compare it against a relevant benchmark index (e.g., NIFTY IT, NIFTY BANK, NIFTY PHARMA). The implementation follows the existing SPA architecture, design system, and backend patterns used in OpenCase.

Problem

Previously, the Investment Detail view only showed point-in-time metrics:

  • Invested amount

  • Current value

  • Total P&L

There was no way to:

  • Visualize performance over time

  • Compare portfolio returns to a benchmark index

  • Analyze performance across different time ranges (1M, 3M, 6M, 1Y, ALL)

This limited a user’s ability to evaluate strategy quality and understand risk-adjusted performance.

Solution

  1. New Backend Endpoint
    Endpoint

GET /api/investments/:id/performance

Query parameters

  • period: 1M | 3M | 6M | 1Y | ALL (default: 1Y)

  • benchmark (optional): override benchmark symbol

Responsibilities

  • Validate that the investment belongs to the authenticated user.

  • Compute the date range based on period, clamped to invested_at.

  • Read daily snapshots from investment_history for the given investment_id.

  • Read benchmark prices from benchmark_data using:

  1. baskets.benchmark_symbol for the investment’s basket, or

  2. the optional benchmark query param.

  • Build a shared date axis and forward‑fill missing days for both series.

  • Normalize both portfolio and benchmark series to base 100 at the start date.

  • Return a compact time-series payload:

{
  "success": true,
  "data": {
    "dates": ["2025-04-12", "2025-04-13", "..."],
    "values": [100.0, 100.45, 100.92, "..."],
    "benchmark_values": [100.0, 100.37, 100.81, "..."],
    "benchmark_name": "NIFTY BANK"
  },
  "error": null,
  "timestamp": "..."
}
  1. Key points
  • Uses investment_history and benchmark_data tables (no schema changes).

  • All queries are parameterized.

  • Handles “no data” cases explicitly with a NO_DATA error.

  • Normalization logic is in a small utility function for reuse and clarity.

  1. Frontend: Investment Detail Performance Chart
    Location

Investment Detail view (state.currentView === 'investment-detail').

UI/UX

  • New “Performance” card under the existing summary metrics.

  • Time range selector with buttons:

  • 1M, 3M, 6M, 1Y, ALL

  • Chart.js line chart:

  1. Portfolio line: solid indigo with a light fill.

  2. Benchmark line: dashed gray line.

  3. Y‑axis label: “Normalized to 100”.

  • Legend on the top‑right: “Portfolio” + benchmark name.
  • Styling matches existing cards (rounded corners, shadows, spacing, typography, colors).

Behavior

  • When an investment is opened, the chart auto‑loads with period=1Y.

  • Clicking a period button:

  1. Updates the active button styling (indigo background, white text).

  2. Refetches /api/investments/:id/performance?period=....

  3. Destroys and recreates the Chart.js instance with new data.

  • Loading state:

Semi‑transparent overlay with spinner inside the chart card.

  • Error state:

Centered message in the card if performance data is not available.

  1. Data Seeding (Local/Dev Only)

For easier local development and visual verification, a seed script populates:

  • investments with three sample investments (Tech Leaders, Banking Giants, Pharma Champions).

  • investment_history with 4–8 months of daily data per investment.

  • benchmark_data with ~1 year of daily prices for:

  • NIFTY 50

  • NIFTY IT

  • NIFTY BANK

  • NIFTY PHARMA

This script is intended for local/dev environments only and does not alter the schema.

Files Touched (high level)
Backend

  • src/routes/investments.ts

  • Add GET /api/investments/:id/performance route.

  • Query investment_history and benchmark_data.

  • Normalize and return time‑series response.

  • (If present) src/lib/utils.ts

  • Utility to normalize numeric series to base 100.

  • Utility to format dates (e.g., YYYY-MM-DD).

Frontend

public/static/app.js

  • Enhance renderInvestmentDetail() to include:

  • Performance card

  • Period selector buttons

  • Chart container

New helpers:

  • loadPerformanceChart(investmentId, period)

  • renderPerformanceChart(data) – Chart.js configuration

  • renderInvestmentHoldings(investment) if not already present / used here.

Impact and Risk

  • Schema changes: None.

  • Existing endpoints: Unchanged.

  • UI: Only the Investment Detail view is extended; other views are unaffected.

  • Risk level: Low

  • New endpoint is additive.

  • Frontend change is isolated to one view.

  • Seed data only affects local/dev environments.

Rollback is straightforward: remove the performance endpoint and the performance card/chart block from the Investment Detail view.

Screenshot (62) Screenshot (61)

Summary by cubic

Adds a historical performance chart to the Investment Detail page with a benchmark comparison so users can see returns over time and compare against an index. Includes a new backend endpoint and a Chart.js UI with period filters.

  • New Features

    • Backend: GET /api/investments/:id/performance?period=1M|3M|6M|1Y|ALL&benchmark=... returns normalized (base 100) portfolio and benchmark series; validates ownership, builds a shared date axis, and forward-fills missing days. Default benchmark comes from the basket; override via benchmark param.
    • Frontend: New “Performance” card with period buttons (1M, 3M, 6M, 1Y, ALL), a Chart.js line chart (solid portfolio vs dashed benchmark), auto-loads 1Y, with loading and error states.
  • Migration

    • No schema changes.
    • Chart.js added via CDN in src/index.tsx.
    • Optional: seed local data with scripts/seed-final.sql for sample investments, histories, and NIFTY benchmarks.

Written for commit afd3754. Summary will update automatically on new commits.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 4 files

Prompt for AI agents (all 6 issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/routes/investments.ts">

<violation number="1" location="src/routes/investments.ts:1689">
P1: Date calculation bug: `startDate.setMonth(endDate.getMonth() - 1)` incorrectly modifies the month of `investment.invested_at` rather than calculating 1 month before `endDate`. This results in wrong date ranges. Initialize `startDate` from `endDate` instead, then subtract the period.</violation>

<violation number="2" location="src/routes/investments.ts:1750">
P1: Remove debug `console.log` statements before merging to production. These will clutter logs and may expose sensitive financial data like investment values and benchmark prices.</violation>

<violation number="3" location="src/routes/investments.ts:1772">
P2: Magic number `21000` as benchmark fallback is problematic. Different benchmarks (NIFTY BANK, NIFTY IT, NIFTY PHARMA) have vastly different price levels. Consider returning an error or omitting benchmark data when no benchmark records exist.</violation>
</file>

<file name="scripts/seed-final.sql">

<violation number="1" location="scripts/seed-final.sql:10">
P2: Use a clearly fake test email address instead of what appears to be a real personal email. Consider using a placeholder like `test@example.com` or `dev@localhost` to avoid privacy concerns and accidental email delivery.</violation>

<violation number="2" location="scripts/seed-final.sql:74">
P1: This `DELETE FROM investment_history;` unconditionally removes ALL records, which could be catastrophic if accidentally run against production. Consider either: (1) deleting only seeded test data by filtering on specific investment_ids, or (2) using `INSERT OR REPLACE` instead of DELETE+INSERT to maintain consistency with the `INSERT OR IGNORE` pattern used elsewhere.</violation>
</file>

<file name="public/static/app.js">

<violation number="1" location="public/static/app.js:2987">
P2: Loading state overlay is positioned outside the relative container. The `absolute inset-0` on `chartLoading` won&#39;t overlay the chart correctly because it&#39;s a sibling of the relative div, not a child. Move it inside the relative container.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Ask questions if you need clarification on any suggestion

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

ORDER BY recorded_date ASC
`).bind(benchmarkSymbol, startDateStr, endDateStr).all<BenchmarkData>();

console.log('[DEBUG] Benchmark history count:', benchmarkHistory.results?.length);
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Remove debug console.log statements before merging to production. These will clutter logs and may expose sensitive financial data like investment values and benchmark prices.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/investments.ts, line 1750:

<comment>Remove debug `console.log` statements before merging to production. These will clutter logs and may expose sensitive financial data like investment values and benchmark prices.</comment>

<file context>
@@ -1635,4 +1636,185 @@ investments.get(&#39;/:id/rebalance-history&#39;, async (c) =&gt; {
+  ORDER BY recorded_date ASC
+`).bind(benchmarkSymbol, startDateStr, endDateStr).all&lt;BenchmarkData&gt;();
+
+console.log(&#39;[DEBUG] Benchmark history count:&#39;, benchmarkHistory.results?.length);
+console.log(&#39;[DEBUG] First benchmark record:&#39;, benchmarkHistory.results?.[0]);
+console.log(&#39;[DEBUG] Last benchmark record:&#39;, benchmarkHistory.results?.[benchmarkHistory.results?.length - 1]);
</file context>

✅ Addressed in afd3754


switch (period) {
case '1M':
startDate.setMonth(endDate.getMonth() - 1);
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Date calculation bug: startDate.setMonth(endDate.getMonth() - 1) incorrectly modifies the month of investment.invested_at rather than calculating 1 month before endDate. This results in wrong date ranges. Initialize startDate from endDate instead, then subtract the period.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/investments.ts, line 1689:

<comment>Date calculation bug: `startDate.setMonth(endDate.getMonth() - 1)` incorrectly modifies the month of `investment.invested_at` rather than calculating 1 month before `endDate`. This results in wrong date ranges. Initialize `startDate` from `endDate` instead, then subtract the period.</comment>

<file context>
@@ -1635,4 +1636,185 @@ investments.get(&#39;/:id/rebalance-history&#39;, async (c) =&gt; {
+    
+    switch (period) {
+      case &#39;1M&#39;:
+        startDate.setMonth(endDate.getMonth() - 1);
+        break;
+      case &#39;3M&#39;:
</file context>

✅ Addressed in afd3754

const investmentValues: number[] = [];
const benchmarkValues: number[] = [];
let lastInvestmentValue = investmentHistory.results[0].current_value;
let lastBenchmarkValue = benchmarkHistory.results?.[0]?.close_price || 21000; // Use realistic default
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Magic number 21000 as benchmark fallback is problematic. Different benchmarks (NIFTY BANK, NIFTY IT, NIFTY PHARMA) have vastly different price levels. Consider returning an error or omitting benchmark data when no benchmark records exist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/investments.ts, line 1772:

<comment>Magic number `21000` as benchmark fallback is problematic. Different benchmarks (NIFTY BANK, NIFTY IT, NIFTY PHARMA) have vastly different price levels. Consider returning an error or omitting benchmark data when no benchmark records exist.</comment>

<file context>
@@ -1635,4 +1636,185 @@ investments.get(&#39;/:id/rebalance-history&#39;, async (c) =&gt; {
+const investmentValues: number[] = [];
+const benchmarkValues: number[] = [];
+let lastInvestmentValue = investmentHistory.results[0].current_value;
+let lastBenchmarkValue = benchmarkHistory.results?.[0]?.close_price || 21000; // Use realistic default
+
+dates.forEach((date, index) =&gt; {
</file context>

✅ Addressed in afd3754

id, zerodha_user_id, name, email, broker_type,
is_primary, is_active, created_at, updated_at
) VALUES (
1, 'AB1234', 'Prashant Trading', 'focusedprshnt@gmail.com', 'zerodha',
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Use a clearly fake test email address instead of what appears to be a real personal email. Consider using a placeholder like test@example.com or dev@localhost to avoid privacy concerns and accidental email delivery.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/seed-final.sql, line 10:

<comment>Use a clearly fake test email address instead of what appears to be a real personal email. Consider using a placeholder like `test@example.com` or `dev@localhost` to avoid privacy concerns and accidental email delivery.</comment>

<file context>
@@ -0,0 +1,106 @@
+  id, zerodha_user_id, name, email, broker_type, 
+  is_primary, is_active, created_at, updated_at
+) VALUES (
+  1, &#39;AB1234&#39;, &#39;Prashant Trading&#39;, &#39;focusedprshnt@gmail.com&#39;, &#39;zerodha&#39;,
+  1, 1, datetime(&#39;now&#39;), datetime(&#39;now&#39;)
+);
</file context>

✅ Addressed in afd3754

(3, 'DIVISLAB', 'NSE', 2, 4000.00, 4200.00, 20.0, 20.0, 400, 5.0, datetime('now'));

-- Step 7: Populate investment_history
DELETE FROM investment_history;
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This DELETE FROM investment_history; unconditionally removes ALL records, which could be catastrophic if accidentally run against production. Consider either: (1) deleting only seeded test data by filtering on specific investment_ids, or (2) using INSERT OR REPLACE instead of DELETE+INSERT to maintain consistency with the INSERT OR IGNORE pattern used elsewhere.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/seed-final.sql, line 74:

<comment>This `DELETE FROM investment_history;` unconditionally removes ALL records, which could be catastrophic if accidentally run against production. Consider either: (1) deleting only seeded test data by filtering on specific investment_ids, or (2) using `INSERT OR REPLACE` instead of DELETE+INSERT to maintain consistency with the `INSERT OR IGNORE` pattern used elsewhere.</comment>

<file context>
@@ -0,0 +1,106 @@
+(3, &#39;DIVISLAB&#39;, &#39;NSE&#39;, 2, 4000.00, 4200.00, 20.0, 20.0, 400, 5.0, datetime(&#39;now&#39;));
+
+-- Step 7: Populate investment_history
+DELETE FROM investment_history;
+INSERT INTO investment_history (investment_id, recorded_date, invested_amount, current_value, day_change, day_change_percentage, total_pnl, total_pnl_percentage, created_at)
+SELECT i.id, date(i.invested_at, &#39;+&#39; || n.day || &#39; days&#39;), i.invested_amount, 
</file context>

✅ Addressed in afd3754

</div>
<!-- Loading State -->
<div id="chartLoading" class="hidden absolute inset-0 flex items-center justify-center bg-white bg-opacity-75">
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Loading state overlay is positioned outside the relative container. The absolute inset-0 on chartLoading won't overlay the chart correctly because it's a sibling of the relative div, not a child. Move it inside the relative container.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At public/static/app.js, line 2987:

<comment>Loading state overlay is positioned outside the relative container. The `absolute inset-0` on `chartLoading` won&#39;t overlay the chart correctly because it&#39;s a sibling of the relative div, not a child. Move it inside the relative container.</comment>

<file context>
@@ -2787,122 +2818,207 @@ async function refreshBasketPrices(basketId) {
+        &lt;/div&gt;
+
+        &lt;!-- Loading State --&gt;
+        &lt;div id=&quot;chartLoading&quot; class=&quot;hidden absolute inset-0 flex items-center justify-center bg-white bg-opacity-75&quot;&gt;
+          &lt;i class=&quot;fas fa-spinner fa-spin text-4xl text-indigo-600&quot;&gt;&lt;/i&gt;
+        &lt;/div&gt;
</file context>

✅ Addressed in afd3754

@prshntrajput prshntrajput changed the title feat: Add investment performance chart with benchmark comparison feat: Added investment performance chart with benchmark comparison Dec 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant