generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) username String @unique // lowercase, used for URLs/lookups displayUsername String? // original casing chosen by user passwordHash String balance Float @default(2000) researchPoints Int @default(1) isAdmin Boolean @default(false) lastLotteryAt DateTime? // tracks daily lottery cooldown createdAt DateTime @default(now()) updatedAt DateTime @updatedAt isFund Boolean @default(false) isHidden Boolean @default(false) // hidden from leaderboards and public listings positions Position[] trades Trade[] passwordResets PasswordReset[] managedFunds FundManager[] fund HedgeFund? fundInvestments FundInvestment[] portfolioHistory UserPortfolioHistory[] fundApplication FundApplication? } model HedgeFund { id String @id @default(cuid()) name String @unique slug String @unique // lowercase, URL-safe userId String @unique // shadow User account that holds positions/trades/balance user User @relation(fields: [userId], references: [id]) sharesOutstanding Float @default(0) // total fund shares currently in circulation createdAt DateTime @default(now()) managers FundManager[] investments FundInvestment[] navHistory FundNavHistory[] trades Trade[] @@index([slug]) } model FundInvestment { id String @id @default(cuid()) fundId String fund HedgeFund @relation(fields: [fundId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) shares Float // fund shares held by this investor avgNavAtBuy Float // NAV per share at time of purchase (for display only) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([fundId, userId]) @@index([userId]) } model FundManager { id String @id @default(cuid()) fundId String fund HedgeFund @relation(fields: [fundId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) addedAt DateTime @default(now()) @@unique([fundId, userId]) } model PasswordReset { id String @id @default(cuid()) token String @unique @default(uuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) expiresAt DateTime used Boolean @default(false) createdAt DateTime @default(now()) @@index([token]) } model Hashtag { id String @id @default(cuid()) tag String @unique // lowercase, no # displayTag String // original case as entered currentPrice Float @default(0.25) isActive Boolean @default(true) isBanned Boolean @default(false) // Consecutive zero-result count (informational) zeroCount Int @default(0) // Earliest time this hashtag can be deactivated (set on research + when last position closes) activeUntil DateTime? lastUpdated DateTime @default(now()) createdAt DateTime @default(now()) priceHistory PriceHistory[] positions Position[] trades Trade[] relatedFrom RelatedHashtag[] @relation("RelatedFrom") relatedTo RelatedHashtag[] @relation("RelatedTo") @@index([isActive, lastUpdated]) } model RelatedHashtag { id String @id @default(cuid()) hashtagId String hashtag Hashtag @relation("RelatedFrom", fields: [hashtagId], references: [id], onDelete: Cascade) relatedTag String // lowercase tag name (may not yet be in Hashtag table) relatedId String? // set if the related hashtag exists in the DB related Hashtag? @relation("RelatedTo", fields: [relatedId], references: [id], onDelete: SetNull) coOccurrences Int @default(1) updatedAt DateTime @updatedAt @@unique([hashtagId, relatedTag]) @@index([hashtagId, coOccurrences]) } model PriceHistory { id String @id @default(cuid()) hashtagId String hashtag Hashtag @relation(fields: [hashtagId], references: [id], onDelete: Cascade) price Float postsPerHour Float recordedAt DateTime @default(now()) @@index([hashtagId, recordedAt]) } model FundNavHistory { id String @id @default(cuid()) fundId String fund HedgeFund @relation(fields: [fundId], references: [id], onDelete: Cascade) nav Float totalValue Float recordedAt DateTime @default(now()) @@index([fundId, recordedAt]) } model UserPortfolioHistory { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) totalValue Float portfolioValue Float recordedAt DateTime @default(now()) @@index([userId, recordedAt]) } model Position { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) hashtagId String hashtag Hashtag @relation(fields: [hashtagId], references: [id]) shares Float @default(0) positionType PositionType avgBuyPrice Float createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, hashtagId, positionType]) @@index([userId]) @@index([hashtagId]) } model Trade { id String @id @default(cuid()) userId String 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) total Float // cost/proceeds of the trade profit Float @default(0) // realized P&L (for SELL trades and LOTTERY_WIN) createdAt DateTime @default(now()) @@index([userId]) @@index([hashtagId]) @@index([fundId]) @@index([createdAt]) } enum PositionType { LONG SHORT } enum TradeType { BUY_LONG SELL_LONG BUY_SHORT SELL_SHORT LOTTERY_WIN LIQUIDATE_LONG LIQUIDATE_SHORT 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 } model FundApplication { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) fundName String reason String createdAt DateTime @default(now()) }