This commit is contained in:
@@ -4,7 +4,11 @@ import { authOptions } from '@/lib/auth'
|
|||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { formatCurrency, formatNumber, formatPnl, pnlColor } from '@/lib/utils'
|
import { formatCurrency, formatNumber, formatPnl, pnlColor } from '@/lib/utils'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Coins } from 'lucide-react'
|
import { Coins, AlertTriangle } from 'lucide-react'
|
||||||
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
|
||||||
|
const ZOMBIE_ZERO_COUNT = parseInt(process.env.ZOMBIE_ZERO_COUNT ?? '1000', 10)
|
||||||
|
const PRICE_UPDATE_INTERVAL_MINUTES = parseInt(process.env.PRICE_UPDATE_INTERVAL_MINUTES ?? '60', 10)
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
@@ -50,6 +54,7 @@ export default async function PositionsPage() {
|
|||||||
tag: true,
|
tag: true,
|
||||||
displayTag: true,
|
displayTag: true,
|
||||||
currentPrice: true,
|
currentPrice: true,
|
||||||
|
zeroCount: true,
|
||||||
priceHistory: {
|
priceHistory: {
|
||||||
orderBy: { recordedAt: 'asc' },
|
orderBy: { recordedAt: 'asc' },
|
||||||
take: 20,
|
take: 20,
|
||||||
@@ -98,6 +103,18 @@ export default async function PositionsPage() {
|
|||||||
const currentValue = pos.hashtag.currentPrice * pos.shares
|
const currentValue = pos.hashtag.currentPrice * pos.shares
|
||||||
const pnlPct = costBasis > 0 ? (pnl / costBasis) * 100 : 0
|
const pnlPct = costBasis > 0 ? (pnl / costBasis) * 100 : 0
|
||||||
const sparkPrices = pos.hashtag.priceHistory.map((h) => h.price)
|
const sparkPrices = pos.hashtag.priceHistory.map((h) => h.price)
|
||||||
|
const isZombieWarning = pos.hashtag.zeroCount >= ZOMBIE_ZERO_COUNT * 0.9
|
||||||
|
const zombieSince = isZombieWarning
|
||||||
|
? formatDistanceToNow(
|
||||||
|
new Date(Date.now() - pos.hashtag.zeroCount * PRICE_UPDATE_INTERVAL_MINUTES * 60 * 1000),
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
const zombieEta = isZombieWarning
|
||||||
|
? formatDistanceToNow(
|
||||||
|
new Date(Date.now() + (ZOMBIE_ZERO_COUNT - pos.hashtag.zeroCount) * PRICE_UPDATE_INTERVAL_MINUTES * 60 * 1000),
|
||||||
|
{ addSuffix: true },
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -114,6 +131,15 @@ export default async function PositionsPage() {
|
|||||||
>
|
>
|
||||||
#{pos.hashtag.displayTag}
|
#{pos.hashtag.displayTag}
|
||||||
</Link>
|
</Link>
|
||||||
|
{isZombieWarning && zombieSince && zombieEta && (
|
||||||
|
<span
|
||||||
|
title={`No activity detected for ${zombieSince}. Estimated auto-liquidation ${zombieEta}.`}
|
||||||
|
className="inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded bg-amber-500/15 text-amber-400 border border-amber-500/20 mt-0.5"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="h-3 w-3" />
|
||||||
|
No activity for {zombieSince} · Liquidates {zombieEta}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span
|
<span
|
||||||
className={`text-xs font-medium px-1.5 py-0.5 rounded ${
|
className={`text-xs font-medium px-1.5 py-0.5 rounded ${
|
||||||
pos.positionType === 'LONG'
|
pos.positionType === 'LONG'
|
||||||
|
|||||||
Reference in New Issue
Block a user