diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d47643a..6ca2939 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,6 +43,7 @@ model HedgeFund { managers FundManager[] investments FundInvestment[] navHistory FundNavHistory[] + trades Trade[] @@index([slug]) } @@ -178,6 +179,8 @@ model Trade { user User @relation(fields: [userId], references: [id], onDelete: Cascade) hashtagId String? hashtag Hashtag? @relation(fields: [hashtagId], references: [id]) + fundId String? + fund HedgeFund? @relation(fields: [fundId], references: [id], onDelete: SetNull) type TradeType shares Float price Float // price per share at time of trade (or win amount for LOTTERY_WIN) @@ -187,6 +190,7 @@ model Trade { @@index([userId]) @@index([hashtagId]) + @@index([fundId]) @@index([createdAt]) } @@ -206,4 +210,6 @@ enum TradeType { DONATION // keepHistory reset: user was in the green — donated their portfolio BANKRUPTCY // keepHistory reset: user was in the red — debts cleared ACCOUNT_OPEN // keepHistory reset: new $2000 account opening entry + FUND_INVEST // invested cash into a hedge fund + FUND_REDEEM // redeemed shares from a hedge fund } diff --git a/src/app/api/funds/[slug]/invest/route.ts b/src/app/api/funds/[slug]/invest/route.ts index 43ae891..2d63b53 100644 --- a/src/app/api/funds/[slug]/invest/route.ts +++ b/src/app/api/funds/[slug]/invest/route.ts @@ -79,6 +79,10 @@ export async function POST(req: NextRequest, { params }: { params: { slug: strin }), // Increment fund shares outstanding prisma.hedgeFund.update({ where: { id: fund.id }, data: { sharesOutstanding: { increment: sharesToMint } } }), + // Log trade in activity history + prisma.trade.create({ + data: { userId: session.user.id, fundId: fund.id, type: 'FUND_INVEST', shares: sharesToMint, price: nav, total: amount, profit: 0 }, + }), ]) return NextResponse.json({ diff --git a/src/app/api/funds/[slug]/redeem/route.ts b/src/app/api/funds/[slug]/redeem/route.ts index cc9955e..4f11d05 100644 --- a/src/app/api/funds/[slug]/redeem/route.ts +++ b/src/app/api/funds/[slug]/redeem/route.ts @@ -35,7 +35,7 @@ export async function POST(req: NextRequest, { params }: { params: { slug: strin const investment = await prisma.fundInvestment.findUnique({ where: { fundId_userId: { fundId: fund.id, userId: session.user.id } }, - select: { shares: true }, + select: { shares: true, avgNavAtBuy: true }, }) if (!investment || investment.shares < sharesToRedeem) { @@ -57,6 +57,7 @@ export async function POST(req: NextRequest, { params }: { params: { slug: strin } const remainingShares = Math.round((investment.shares - sharesToRedeem) * 1e6) / 1e6 + const profit = round2(payout - sharesToRedeem * investment.avgNavAtBuy) const [updatedInvestor] = await prisma.$transaction([ // Return cash to investor @@ -74,6 +75,10 @@ export async function POST(req: NextRequest, { params }: { params: { slug: strin })]), // Decrement fund shares outstanding prisma.hedgeFund.update({ where: { id: fund.id }, data: { sharesOutstanding: { decrement: sharesToRedeem } } }), + // Log trade in activity history + prisma.trade.create({ + data: { userId: session.user.id, fundId: fund.id, type: 'FUND_REDEEM', shares: sharesToRedeem, price: nav, total: payout, profit }, + }), ]) return NextResponse.json({ diff --git a/src/app/history/page.tsx b/src/app/history/page.tsx index 66e5c94..d564fad 100644 --- a/src/app/history/page.tsx +++ b/src/app/history/page.tsx @@ -30,6 +30,7 @@ export default async function TradeHistoryPage({ searchParams }: PageProps) { skip: (page - 1) * PAGE_SIZE, include: { hashtag: { select: { tag: true, displayTag: true } }, + fund: { select: { name: true, slug: true } }, }, }), ]) @@ -61,6 +62,7 @@ export default async function TradeHistoryPage({ searchParams }: PageProps) { const isLottery = t.type === 'LOTTERY_WIN' const isLiquidation = t.type === 'LIQUIDATE_LONG' || t.type === 'LIQUIDATE_SHORT' const isSystemReset = t.type === 'DONATION' || t.type === 'BANKRUPTCY' || t.type === 'ACCOUNT_OPEN' + const isFundTrade = t.type === 'FUND_INVEST' || t.type === 'FUND_REDEEM' return (
{formatPnl(t.profit)}
> + ) : isFundTrade ? ( + <> +{formatNumber(t.shares, 6)} sh @ {formatCurrency(t.price)}
+{formatCurrency(t.total)}
+ {t.type === 'FUND_REDEEM' && ( +{formatPnl(t.profit)}
+ )} + > ) : ( <>{formatNumber(t.shares)} sh @ {formatCurrency(t.price)}
diff --git a/src/app/positions/page.tsx b/src/app/positions/page.tsx index f2e2cd5..5b5a8cf 100644 --- a/src/app/positions/page.tsx +++ b/src/app/positions/page.tsx @@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { redirect } from 'next/navigation' import { formatCurrency, formatNumber, formatPnl, pnlColor } from '@/lib/utils' +import { calcFundNav } from '@/lib/pricing' import Link from 'next/link' import { Coins, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react' import { AutoRefresh } from '@/components/AutoRefresh' @@ -96,6 +97,49 @@ export default async function PositionsPage({ }, }) + const rawFundInvestments = await prisma.fundInvestment.findMany({ + where: { userId: session.user.id, shares: { gt: 0 } }, + include: { + fund: { + select: { + name: true, + slug: true, + sharesOutstanding: true, + user: { + select: { + balance: true, + positions: { + where: { shares: { gt: 0 } }, + select: { + shares: true, + avgBuyPrice: true, + positionType: true, + hashtag: { select: { currentPrice: true } }, + }, + }, + }, + }, + }, + }, + }, + }) + + const fundHoldings = rawFundInvestments.map((inv) => { + const fundPortfolioValue = inv.fund.user.positions.reduce((sum, p) => { + const val = p.positionType === 'LONG' + ? p.shares * p.hashtag.currentPrice + : (2 * p.avgBuyPrice - p.hashtag.currentPrice) * p.shares + return sum + val + }, 0) + const fundTotalValue = inv.fund.user.balance + fundPortfolioValue + const nav = calcFundNav(fundTotalValue, inv.fund.sharesOutstanding) + const currentValue = inv.shares * nav + const costBasis = inv.shares * inv.avgNavAtBuy + const pnl = currentValue - costBasis + const pnlPct = costBasis > 0 ? (pnl / costBasis) * 100 : 0 + return { ...inv, nav, currentValue, costBasis, pnl, pnlPct } + }) + const positions = rawPositions.map((pos) => { const pnl = pos.positionType === 'LONG' @@ -137,7 +181,7 @@ export default async function PositionsPage({You have no open positions.
@@ -206,6 +250,41 @@ export default async function PositionsPage({{formatPnl(inv.pnl)}
+{inv.pnlPct >= 0 ? '+' : ''}{inv.pnlPct.toFixed(1)}%
+