diff --git a/backend/scheduler/src/jobs/weekly-portfolio-updates.ts b/backend/scheduler/src/jobs/weekly-portfolio-updates.ts deleted file mode 100644 index 0ab9608b56..0000000000 --- a/backend/scheduler/src/jobs/weekly-portfolio-updates.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { sum } from 'lodash' - -import { getUsersContractMetricsOrderedByProfit } from 'common/supabase/contract-metrics' -import { createWeeklyPortfolioUpdateNotification } from 'shared/create-notification' -import { - createSupabaseClient, - createSupabaseDirectClient, - SupabaseClient, -} from 'shared/supabase/init' -import { getUsers, log } from 'shared/utils' -import { bulkInsert } from 'shared/supabase/utils' -import { APIError } from 'common/api/utils' - -import * as dayjs from 'dayjs' -import { Row } from 'common/supabase/utils' -import { ContractMetric } from 'common/contract-metric' -import { convertPrivateUser } from 'common/supabase/users' - -const now = new Date() -const time = now.getTime() - -const getDate = () => dayjs(now).format('YYYY-MM-DD') - -const USERS_TO_SAVE = 300 -// // Saving metrics should work until our users are greater than USERS_TO_SAVE * 2*60 users -// export const saveWeeklyContractMetrics = functions -// .runWith({ memory: '4GB', secrets, timeoutSeconds: 60 }) -// // every minute for 2 hours Friday 4am PT (UTC -08:00) -// .pubsub.schedule('* 13-14 * * 5') -// .timeZone('Etc/UTC') -// .onRun(async () => { -// await saveWeeklyContractMetricsInternal() -// }) - -// export const sendWeeklyPortfolioUpdate = functions -// .runWith({ memory: '8GB', secrets, timeoutSeconds: 540 }) -// // every Friday at 12pm PT (UTC -08:00) -// .pubsub.schedule('0 20 * * 5') -// .timeZone('Etc/UTC') -// .onRun(async () => { -// await sendWeeklyPortfolioUpdateNotifications() -// }) - -// NOTE: this isn't used anywhere -export const saveWeeklyContractMetricsInternal = async () => { - const db = createSupabaseClient() - - // users who have disabled browser notifications for profit/loss updates won't be able to see their portfolio updates in the past - const privateUsersQuery = await db - .from('private_users') - .select('id') - .contains( - `data->'notificationPreferences'->'profit_loss_updates'`, - 'browser' - ) - - if (privateUsersQuery.error) { - throw new APIError( - 500, - 'Error getting private users: ', - privateUsersQuery.error - ) - } - const privateUsers = privateUsersQuery.data - - const alreadyUpdatedQuery = await db - .from('weekly_update') - .select('user_id') - .eq('range_end', getDate()) - - if (alreadyUpdatedQuery.error) { - throw new APIError( - 500, - 'Error getting already updated users: ', - alreadyUpdatedQuery.error - ) - } - - const alreadyUpdated = alreadyUpdatedQuery.data.map((r) => r.user_id) - - log('already updated users', alreadyUpdated.length, 'at', time) - // filter out the users who have already had their weekly update saved - const usersToSave = privateUsers - .filter((user) => !alreadyUpdated.includes(user.id)) - .slice(0, USERS_TO_SAVE) - - log('usersToSave', usersToSave.length) - if (usersToSave.length === 0) return - - // TODO: try out the new rpc call - const usersToContractMetrics = await getUsersContractMetricsOrderedByProfit( - usersToSave.map((u) => u.id), - db, - 'week' - ) - if (Object.keys(usersToContractMetrics).length === 0) { - log('Error: no contract metrics to save') - return - } - - const results = await Promise.all( - usersToSave.map(async (privateUser) => { - const contractMetrics = usersToContractMetrics[privateUser.id] - return { - contract_metrics: contractMetrics, - user_id: privateUser.id, - profit: sum(contractMetrics.map((m) => m.from?.week.profit ?? 0)), - range_end: getDate(), - } - }) - ) - - const pg = createSupabaseDirectClient() - await bulkInsert(pg, 'weekly_update', results) - - log('saved weekly contract metrics for users:', usersToSave.length) -} - -export const sendWeeklyPortfolioUpdateNotifications = async () => { - const db = createSupabaseClient() - - // get all users who have opted in to weekly portfolio updates - const privateUsersQuery = await db - .from('private_users') - .select() - .contains( - `data->'notificationPreferences'->'profit_loss_updates'`, - 'browser' - ) - - if (privateUsersQuery.error) { - throw new APIError( - 500, - 'Error getting private users: ', - privateUsersQuery.error - ) - } - const privateUsers = privateUsersQuery.data.map(convertPrivateUser) - - const userData = await getUsers(privateUsers.map((u) => u.id)) - const usernameById = Object.fromEntries( - userData.map((u) => [u.id, u.username]) - ) - log('users to send weekly portfolio updates to', privateUsers.length) - let count = 0 - const now = getDate() - await Promise.all( - privateUsers.map(async (privateUser) => { - const data = await getUsersWeeklyUpdate(db, privateUser.id, now) - if (!data) return - const { profit, range_end, contract_metrics } = data - const contractMetrics = contract_metrics as ContractMetric[] - // Don't send update if there are no contracts - count++ - if (count % 100 === 0) - log('sent weekly portfolio updates to', count, '/', privateUsers.length) - if (contractMetrics.length === 0) return - await createWeeklyPortfolioUpdateNotification( - privateUser, - usernameById[privateUser.id], - profit, - range_end - ) - }) - ) -} - -const getUsersWeeklyUpdate = async ( - db: SupabaseClient, - userId: string, - rangeEnd: string -) => { - const { data, error } = await db - .from('weekly_update') - .select('*') - .eq('user_id', userId) - .eq('range_end', rangeEnd) - .order('created_time', { ascending: false }) - .limit(1) - - if (error) { - console.error(error) - return - } - if (!data.length) { - return - } - - return data[0] as Row<'weekly_update'> -} diff --git a/backend/supabase/weekly_update.sql b/backend/supabase/weekly_update.sql deleted file mode 100644 index 2deca03db7..0000000000 --- a/backend/supabase/weekly_update.sql +++ /dev/null @@ -1,29 +0,0 @@ --- This file is autogenerated from regen-schema.ts -create table if not exists - weekly_update ( - contract_metrics json not null, - created_time timestamp with time zone default now() not null, - id text primary key default random_alphanumeric (12) not null, - profit numeric not null, - range_end date not null, - user_id text not null - ); - --- Foreign Keys -alter table weekly_update -add constraint weekly_update_user_id_fkey foreign key (user_id) references users (id); - --- Row Level Security -alter table weekly_update enable row level security; - --- Policies -drop policy if exists "public read" on weekly_update; - -create policy "public read" on weekly_update for -select - using (true); - --- Indexes -drop index if exists weekly_update_pkey; - -create unique index weekly_update_pkey on public.weekly_update using btree (id); diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index 9b91603878..2674163901 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -3152,48 +3152,6 @@ export type Database = { } Relationships: [] } - weekly_update: { - Row: { - contract_metrics: Json - created_time: string - id: string - profit: number - range_end: string - user_id: string - } - Insert: { - contract_metrics: Json - created_time?: string - id?: string - profit: number - range_end: string - user_id: string - } - Update: { - contract_metrics?: Json - created_time?: string - id?: string - profit?: number - range_end?: string - user_id?: string - } - Relationships: [ - { - foreignKeyName: 'weekly_update_user_id_fkey' - columns: ['user_id'] - isOneToOne: false - referencedRelation: 'user_referrals_profit' - referencedColumns: ['id'] - }, - { - foreignKeyName: 'weekly_update_user_id_fkey' - columns: ['user_id'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['id'] - } - ] - } } Views: { final_pp_balances: { diff --git a/web/pages/week/[username]/[rangeEndDateSlug].tsx b/web/pages/week/[username]/[rangeEndDateSlug].tsx deleted file mode 100644 index 707431cd6b..0000000000 --- a/web/pages/week/[username]/[rangeEndDateSlug].tsx +++ /dev/null @@ -1,293 +0,0 @@ -import clsx from 'clsx' -import { HistoryPoint } from 'common/chart' -import { BinaryContract } from 'common/contract' -import { ContractMetric } from 'common/contract-metric' -import { ENV_CONFIG, TRADING_TERM } from 'common/envs/constants' -import { PortfolioMetrics } from 'common/portfolio-metrics' -import { getContracts } from 'common/supabase/contracts' -import { getPortfolioHistory } from 'common/supabase/portfolio-metrics' -import { tsToMillis } from 'common/supabase/utils' -import { formatMoney, formatMoneyNumber } from 'common/util/format' -import { DAY_MS } from 'common/util/time' -import { - WeeklyPortfolioUpdate, - WeeklyPortfolioUpdateOGCardProps, -} from 'common/weekly-portfolio-update' -import { chunk, sortBy, sum } from 'lodash' -import { useLayoutEffect, useMemo } from 'react' -import { CopyLinkOrShareButton } from 'web/components/buttons/copy-link-button' -import { ZoomParams } from 'web/components/charts/helpers' -import { ContractsGrid } from 'web/components/contract/contracts-grid' -import { ProfitChangeTable } from 'web/components/home/daily-profit' -import { Col } from 'web/components/layout/col' -import { Page } from 'web/components/layout/page' -import { Row } from 'web/components/layout/row' -import { PortfolioTooltip } from 'web/components/portfolio/portfolio-value-graph' -import { SEO } from 'web/components/SEO' -import { SizedContainer } from 'web/components/sized-container' -import { LoadingIndicator } from 'web/components/widgets/loading-indicator' -import { Title } from 'web/components/widgets/title' -import { UserLink } from 'web/components/widgets/user-link' -import { useRecentlyBetOnContracts } from 'web/lib/supabase/bets' -import { db } from 'web/lib/supabase/db' -import { DisplayUser, getUserByUsername } from 'web/lib/supabase/users' -import Custom404 from 'web/pages/404' -import { max, min } from 'lodash' -import { Period } from 'common/period' -import { scaleLinear, scaleTime } from 'd3-scale' -import { SingleValueHistoryChart } from 'web/components/charts/generic-charts' -import { curveLinear } from 'd3-shape' - -export async function getStaticProps(props: { - params: { username: string; rangeEndDateSlug: string } -}) { - const { username, rangeEndDateSlug } = props.params - - const user = (await getUserByUsername(username)) ?? null - - const weeklyPortfolioUpdates = user - ? ( - await db - .from('weekly_update') - .select() - .eq('user_id', user?.id) - .eq('range_end', rangeEndDateSlug) - .order('created_time', { ascending: false }) - .limit(1) - ).data - : null - - const weeklyPortfolioUpdate = weeklyPortfolioUpdates?.[0] - const { created_time, contract_metrics } = weeklyPortfolioUpdate ?? {} - const end = created_time - ? tsToMillis(created_time) - : new Date(rangeEndDateSlug).valueOf() - const start = end - 7 * DAY_MS - const profitPoints = - weeklyPortfolioUpdate && user - ? await getPortfolioHistory(user.id, start, db, end).then( - (portfolioHistory) => { - return portfolioHistory?.map((p) => ({ - x: p.timestamp, - y: p.balance + p.investmentValue - p.totalDeposits, - obj: p, - })) - } - ) - : [] - const contracts = weeklyPortfolioUpdate - ? await getContracts( - db, - (contract_metrics as ContractMetric[]).map((c) => c.contractId) - ) - : null - - return { - props: { - user, - profitPoints: sortBy(profitPoints, (p) => p.x), - weeklyPortfolioUpdateString: - JSON.stringify(weeklyPortfolioUpdate) ?? '{}', - contractsString: JSON.stringify(contracts), - }, - - revalidate: 60 * 60, // regenerate after an hour - } -} -const averagePointsInChunks = (points: { x: number; y: number }[]) => { - // Smaller chunks sizes may result in the og image not working bc the url is too long - const chunkSize = 3 - const chunks = chunk(points, chunkSize) - return chunks.map((c) => { - const sumY = sum(c.map((p) => p.y)) - const avgY = sumY / chunkSize - return { x: c[0].x, y: avgY } - }) -} - -export async function getStaticPaths() { - return { paths: [], fallback: 'blocking' } -} - -export default function RangePerformancePage(props: { - user: DisplayUser | null - weeklyPortfolioUpdateString: string - contractsString: string - profitPoints: HistoryPoint>[] -}) { - const { user, weeklyPortfolioUpdateString, profitPoints } = props - const weeklyPortfolioUpdate = JSON.parse( - weeklyPortfolioUpdateString - ) as WeeklyPortfolioUpdate - const contracts = JSON.parse(props.contractsString) as BinaryContract[] - - const { contracts: relatedMarkets, loadMore } = useRecentlyBetOnContracts( - user?.id ?? '_' - ) - - if (!user || !weeklyPortfolioUpdate || !weeklyPortfolioUpdate.weeklyProfit) - return - - const { contractMetrics, weeklyProfit, rangeEndDateSlug, createdTime } = - weeklyPortfolioUpdate - - // eslint-disable-next-line react-hooks/rules-of-hooks - const graphPoints = useMemo(() => { - if (profitPoints.length === 0) return [] - const firstPointToScaleBy = profitPoints[0]?.y ?? 0 - return profitPoints.map((p) => { - const y = p.y - firstPointToScaleBy - return { x: p.x, y, obj: p.obj } - }) - }, [profitPoints]) - - const endDate = createdTime ? new Date(createdTime) : new Date() - const startDate = endDate.getTime() - 7 * DAY_MS - const date = - new Date(startDate).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - }) + - ' - ' + - endDate.toLocaleDateString('en-US', { - day: 'numeric', - }) - const averagePoints = averagePointsInChunks(graphPoints) - const ogProps = { - points: JSON.stringify(averagePoints), - weeklyProfit: weeklyProfit.toString(), - creatorUsername: user.username, - creatorAvatarUrl: user.avatarUrl, - creatorName: user.name, - } as WeeklyPortfolioUpdateOGCardProps - return ( - - - - - - <UserLink user={user} hideBadge={true} /> {date} Profit - - - - - - 0 ? 'text-teal-500' : 'text-scarlet-500' - )} - > - {formatMoney(weeklyProfit)} - - - - {graphPoints.length > 0 && ( - - {(width, height) => ( - - )} - - )} - - - - - Also {TRADING_TERM} on - {relatedMarkets ? ( - - ) : ( - - )} - - - ) -} - -export const ProfitGraph = (props: { - duration?: Period - points: HistoryPoint>[] - width: number - height: number - zoomParams?: ZoomParams - negativeThreshold?: number - hideXAxis?: boolean -}) => { - const { - duration, - points, - width, - height, - zoomParams, - negativeThreshold, - hideXAxis, - } = props - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const minDate = min(points.map((d) => d.x))! - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maxDate = max(points.map((d) => d.x))! - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const minValue = min(points.map((d) => d.y))! - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maxValue = max(points.map((d) => d.y))! - - const tinyDiff = Math.abs(maxValue - minValue) < 20 - const xScale = scaleTime([minDate, maxDate], [0, width]) - const yScale = scaleLinear( - [tinyDiff ? minValue - 50 : minValue, tinyDiff ? maxValue + 50 : maxValue], - [height, 0] - ) - - // reset axis scale if mode or duration change (since points change) - useLayoutEffect(() => { - zoomParams?.setXScale(xScale) - }, [duration]) - - return ( - 768 ? 768 : width} - h={height} - xScale={xScale} - yScale={yScale} - zoomParams={zoomParams} - yKind="Ṁ" - data={points} - // eslint-disable-next-line react/prop-types - Tooltip={(props) => } - curve={curveLinear} - color={['#14b8a6', '#F75836']} - negativeThreshold={negativeThreshold} - hideXAxis={hideXAxis} - /> - ) -}