diff --git a/README.md b/README.md index e9efc47..84dd0d4 100644 --- a/README.md +++ b/README.md @@ -329,59 +329,6 @@ The items below are planned improvements roughly ordered by user value. They are --- -### 1. Auto-Derive Display Casing from Mastodon Posts - -**Problem:** `displayTag` is currently set once at research/creation time from whatever the user typed. In reality Mastodon users have an established "canonical" capitalisation for a tag (e.g. `#StPatricksDay` rather than `#stpatricksday`) and our display tag should reflect that. - -**Plan:** -- During each price update job the worker already fetches up to 200 posts. The Mastodon API returns each post's `tags` array where `name` contains the tag as typed by the poster (e.g. `StPatricksDay`). -- Count the frequency of each distinct casing variant seen across all fetched posts for the hashtag being updated. -- If the most frequent variant differs from the current `displayTag`, update `Hashtag.displayTag` as part of the price update transaction. -- Only update when a variant accounts for a meaningful majority (e.g. ≥ 50% of occurrences) to avoid flip-flopping on low-signal posts. -- Implementation touches: `mastodon.ts` already returns `post.tags` from `getPostsData()` — add a `casing` field to the result (`{ postsPerHour, relatedTags, displayTag? }`); worker checks the returned `displayTag` and includes it in the Prisma update when it differs. -- No schema changes required — `displayTag` already exists on `Hashtag`. - ---- - -### 2. Home Page Holdings Summary (Signed-In) - -**Problem:** Signed-in users land on the home page and see only the generic trending list — there is no personalised hook to show how their portfolio is performing. - -**Plan:** -- Add two summary cards above the "Trending now" section, visible only to signed-in users: **Biggest Gain** and **Biggest Loss** (by unrealised P&L across open positions). -- Both cards link to the relevant hashtag page. -- Implemented as a server component addition to `src/app/page.tsx`: query the current user's open positions (joined with current price) and compute unrealised P&L per position to find the top and bottom. -- If the user has no positions, the cards are omitted (no empty-state clutter). -- No schema changes required. - ---- - -### 3. Search Autocomplete - -**Problem:** The current search/research input is a plain text field with no discovery aid — users must know or guess full hashtag names. - -**Plan:** -- While the user types in the search box, show a dropdown of matching hashtags that are **already tracked** on the exchange (fetched from a new `GET /api/hashtags/search?q=` endpoint). -- The autocomplete only surfaces existing active hashtags; submitting a tag that doesn't appear in the list still proceeds through the normal research flow (no interference with new-hashtag discovery). -- Client-side: debounce the input (~300 ms), cancel in-flight requests on new keystrokes, close dropdown on `Escape` or outside click. -- Endpoint: case-insensitive prefix match on `Hashtag.tag` where `isActive = true` and `isBanned = false`, returning `{ tag, displayTag, currentPrice }` for up to 8 results. -- No schema changes required. - ---- - -### 4. Open Positions Full Page - -**Problem:** The profile page shows open positions in a compact list — there is no space for richer per-position data (cost basis, current value, a mini P&L chart). - -**Plan:** -- New page `/positions` (auth-protected) showing only the signed-in user's open positions in a more detailed layout. -- Each row / card: hashtag name + link, position type (LONG/SHORT), shares held, average buy price, current price, total cost basis, current value, unrealised P&L and P&L %, a sparkline chart of the hashtag's recent price history. -- Link into this page from the "Open positions" heading on the profile page. -- No new nav item needed — accessed via profile page link. -- No schema changes required. - ---- - ### Other Ideas / Nice-to-Haves - **Hedge funds**: group of players pool money into a shared portfolio, one designated fund manager places trades. diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7d87c98..7f03ab6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -105,13 +105,13 @@ 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]) + hashtagId String? + hashtag Hashtag? @relation(fields: [hashtagId], references: [id]) type TradeType shares Float - price Float // price per share at time of trade + 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) + profit Float @default(0) // realized P&L (for SELL trades and LOTTERY_WIN) createdAt DateTime @default(now()) @@index([userId]) @@ -129,4 +129,5 @@ enum TradeType { SELL_LONG BUY_SHORT SELL_SHORT + LOTTERY_WIN } diff --git a/prod-compose.yml b/prod-compose.yml index 1ef09a5..c31b17a 100644 --- a/prod-compose.yml +++ b/prod-compose.yml @@ -29,6 +29,7 @@ services: MASTODON_ACCESS_TOKEN: "${MASTODON_ACCESS_TOKEN}" WORKER_RATE_LIMIT_MS: "${WORKER_RATE_LIMIT_MS:-2000}" PRICE_UPDATE_INTERVAL_MINUTES: "${PRICE_UPDATE_INTERVAL_MINUTES:-60}" + HASHTAG_ACTIVE_HOURS: "${HASHTAG_ACTIVE_HOURS:-24}" MAX_PAGES_PER_HASHTAG: "${MAX_PAGES_PER_HASHTAG:-5}" depends_on: postgres: diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 155d6ab..cc1d3df 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -71,13 +71,17 @@ export default async function AdminOverviewPage() {
{formatNumber(t.shares)} sh @ {formatCurrency(t.price)}
- {(t.type === 'SELL_LONG' || t.type === 'SELL_SHORT') && ( -- {formatPnl(t.profit)} -
+ {isLottery ? ( +{formatCurrency(t.profit)}
+ ) : ( + <> +{formatNumber(t.shares)} sh @ {formatCurrency(t.price)}
+ {(t.type === 'SELL_LONG' || t.type === 'SELL_SHORT') && ( ++ {formatPnl(t.profit)} +
+ )} + > )}