Try to send higher signal emails

This commit is contained in:
Ian Philips 2022-09-26 17:42:06 -04:00
parent 86f4b81f37
commit 73f6ad56e5
4 changed files with 455 additions and 53 deletions

View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Weekly Portfolio Update on Manifold</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family:"Readex Pro", Helvetica, sans-serif;
}
table { margin: 0 auto; }
table,
td {
border-collapse: collapse;
mso-table-lspace: 0;
mso-table-rspace: 0;
}
th {color:#000000; font-size:17px;}
th, td {padding: 10px; }
td{ font-size: 17px}
th, td { vertical-align: center; text-align: left }
a { vertical-align: center; text-align: left}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
p.change{
margin: 0; vertical-align: middle;font-size:16px;display: inline; padding: 2px; border-radius: 5px; width: 20px; text-align: right;
}
p.prob{
font-size: 22px;display: inline; vertical-align: middle; font-weight: bold; width: 50px;
}
a.question{
font-size: 18px;display: inline; vertical-align: middle;
}
td.question{
vertical-align: middle; padding-bottom: 15px; text-align: left;
}
td.probs{
text-align: right; padding-left: 10px; min-width: 115px
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto"
src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align: top; margin-bottom: 30px" width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 0px;"
data-testid="4XoHRGw1Y">
<span style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
We ran the numbers and here's how you did this past week!
</span>
</p>
</div>
</td>
</tr>
<!--/ show 5 columns with headers titled: Investment value, 7-day change, current balance, tips received, and markets made/-->
<tr>
<tr>
<th style='font-size: 22px; text-align: center'>
Profit
</th>
</tr>
<tr>
<td style='padding-bottom: 30px; text-align: center'>
<p class='change' style='font-size: 24px; padding:4px; {{profit_style}}'>
{{profit}}
</p>
</td>
</tr>
<td align="center"
style="font-size:0px;padding:10px 20px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px; ">
<tbody>
<tr>
<th style='width: 170px'>
🔥 Prediction streak
</th>
<td>
{{prediction_streak}}
</td>
</tr>
<tr>
<th>
💸 Tips received
</th>
<td>
{{tips_received}}
</td>
</tr>
<tr>
<th>
📈 Markets traded
</th>
<td>
{{markets_traded}}
</td>
</tr>
<tr>
<th>
❓ Markets created
</th>
<td>
{{markets_created}}
</td>
</tr>
<tr>
<th style='width: 55px'>
🥳 Traders attracted
</th>
<td>
{{unique_bettors}}
</td>
</tr>
</tbody>
</table>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to
{{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -298,7 +298,7 @@
</tr> </tr>
<tr> <tr>
<th style='width: 55px'> <th style='width: 55px'>
🥳 Unique traders 🥳 Traders attracted
</th> </th>
<td> <td>
{{unique_bettors}} {{unique_bettors}}
@ -508,6 +508,3 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</body>
</html>

View File

@ -648,9 +648,12 @@ export const sendWeeklyPortfolioUpdateEmail = async (
}) })
await sendTemplateEmail( await sendTemplateEmail(
privateUser.email, // privateUser.email,
'iansphilips@gmail.com',
`Here's your weekly portfolio update!`, `Here's your weekly portfolio update!`,
'portfolio-update', investments.length === 0
? 'portfolio-update-no-movers'
: 'portfolio-update',
templateData templateData
) )
} }

View File

@ -3,6 +3,7 @@ import * as admin from 'firebase-admin'
import { Contract, CPMMContract } from '../../common/contract' import { Contract, CPMMContract } from '../../common/contract'
import { import {
getAllPrivateUsers,
getPrivateUser, getPrivateUser,
getUser, getUser,
getValue, getValue,
@ -11,9 +12,8 @@ import {
log, log,
} from './utils' } from './utils'
import { filterDefined } from '../../common/util/array' import { filterDefined } from '../../common/util/array'
import { PortfolioMetrics } from '../../common/user'
import { DAY_MS } from '../../common/util/time' import { DAY_MS } from '../../common/util/time'
import { last, partition, sortBy, sum, uniq } from 'lodash' import { partition, sortBy, sum, uniq } from 'lodash'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { computeInvestmentValueCustomProb } from '../../common/calculate-metrics' import { computeInvestmentValueCustomProb } from '../../common/calculate-metrics'
import { sendWeeklyPortfolioUpdateEmail } from './emails' import { sendWeeklyPortfolioUpdateEmail } from './emails'
@ -35,12 +35,12 @@ const firestore = admin.firestore()
export async function sendPortfolioUpdateEmailsToAllUsers() { export async function sendPortfolioUpdateEmailsToAllUsers() {
const privateUsers = isProd() const privateUsers = isProd()
? // TODO: switch back to all private users ? // ian & stephen's ids
// ? await getAllPrivateUsers() // ? filterDefined([
filterDefined([ // await getPrivateUser('AJwLWoo3xue32XIiAVrL5SyR1WB2'),
await getPrivateUser('AJwLWoo3xue32XIiAVrL5SyR1WB2'), // await getPrivateUser('tlmGNz9kjXc2EteizMORes4qvWl2'),
await getPrivateUser('tlmGNz9kjXc2EteizMORes4qvWl2'), // ])
]) await getAllPrivateUsers()
: filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')]) : filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')])
// get all users that haven't unsubscribed from weekly emails // get all users that haven't unsubscribed from weekly emails
const privateUsersToSendEmailsTo = privateUsers const privateUsersToSendEmailsTo = privateUsers
@ -58,18 +58,6 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
'users' 'users'
) )
const usersToPortfolioMetrics: { [userId: string]: PortfolioMetrics[] } = {}
// get all portfolio metrics for each user
await Promise.all(
privateUsersToSendEmailsTo.map(async (user) => {
return getValues<PortfolioMetrics>(
firestore.collection(`users/${user.id}/portfolioHistory`)
).then((portfolioMetrics) => {
return (usersToPortfolioMetrics[user.id] = portfolioMetrics)
})
})
)
const usersBets: { [userId: string]: Bet[] } = {} const usersBets: { [userId: string]: Bet[] } = {}
// get all bets made by each user // get all bets made by each user
await Promise.all( await Promise.all(
@ -125,12 +113,11 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
) )
) )
log('Found', contractsUsersBetOn.length, 'contracts') log('Found', contractsUsersBetOn.length, 'contracts')
let count = 0
await Promise.all( await Promise.all(
privateUsersToSendEmailsTo.map(async (privateUser) => { privateUsersToSendEmailsTo.map(async (privateUser) => {
const user = await getUser(privateUser.id) const user = await getUser(privateUser.id)
if (!user) return if (!user) return
const usersPortfolioMetrics = usersToPortfolioMetrics[privateUser.id]
const userBets = usersBets[privateUser.id] as Bet[] const userBets = usersBets[privateUser.id] as Bet[]
const contractsUserBetOn = contractsUsersBetOn.filter((contract) => const contractsUserBetOn = contractsUsersBetOn.filter((contract) =>
userBets.some((bet) => bet.contractId === contract.id) userBets.some((bet) => bet.contractId === contract.id)
@ -140,24 +127,6 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
.filter((bet) => bet.createdTime > Date.now() - 7 * DAY_MS) .filter((bet) => bet.createdTime > Date.now() - 7 * DAY_MS)
.map((bet) => bet.contractId) .map((bet) => bet.contractId)
) )
const mostRecentPortfolioMetrics = last(
sortBy(
usersPortfolioMetrics,
(portfolioMetric) => portfolioMetric.timestamp
)
)
if (!mostRecentPortfolioMetrics) {
log('No portfolio metrics for user', privateUser.id)
return
}
const portfolioMetricsAWeekAgo = usersPortfolioMetrics.find(
(portfolioMetric) => portfolioMetric.timestamp > Date.now() - 7 * DAY_MS
)
if (!portfolioMetricsAWeekAgo) {
// TODO: send them a no change email?
log('No portfolio metrics a week ago for user', privateUser.id)
return
}
const totalTips = sum( const totalTips = sum(
usersToTxnsReceived[privateUser.id] usersToTxnsReceived[privateUser.id]
.filter((txn) => txn.category === 'TIP') .filter((txn) => txn.category === 'TIP')
@ -165,10 +134,15 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
) )
const greenBg = 'rgba(0,160,0,0.2)' const greenBg = 'rgba(0,160,0,0.2)'
const redBg = 'rgba(160,0,0,0.2)' const redBg = 'rgba(160,0,0,0.2)'
const clearBg = 'rgba(255,255,255,0)'
const roundedProfit =
Math.round(user.profitCached.weekly) === 0
? 0
: Math.floor(user.profitCached.weekly)
const performanceData = { const performanceData = {
profit: formatMoney(user.profitCached.weekly), profit: formatMoney(user.profitCached.weekly),
profit_style: `background-color: ${ profit_style: `background-color: ${
user.profitCached.weekly > 0 ? greenBg : redBg roundedProfit > 0 ? greenBg : roundedProfit === 0 ? clearBg : redBg
}`, }`,
markets_created: markets_created:
usersToContractsCreated[privateUser.id].length.toString(), usersToContractsCreated[privateUser.id].length.toString(),
@ -194,7 +168,9 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
const marketProbabilityAWeekAgo = const marketProbabilityAWeekAgo =
cpmmContract.prob - cpmmContract.probChanges.week cpmmContract.prob - cpmmContract.probChanges.week
const currentMarketProbability = cpmmContract.prob const currentMarketProbability = cpmmContract.resolutionProbability
? cpmmContract.resolutionProbability
: cpmmContract.prob
const betsValueAWeekAgo = computeInvestmentValueCustomProb( const betsValueAWeekAgo = computeInvestmentValueCustomProb(
bets.filter((b) => b.createdTime < Date.now() - 7 * DAY_MS), bets.filter((b) => b.createdTime < Date.now() - 7 * DAY_MS),
contract, contract,
@ -215,7 +191,9 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
marketProbAWeekAgo: marketProbabilityAWeekAgo, marketProbAWeekAgo: marketProbabilityAWeekAgo,
questionTitle: contract.question, questionTitle: contract.question,
questionUrl: contractUrl(contract), questionUrl: contractUrl(contract),
questionProb: Math.round(cpmmContract.prob * 100) + '%', questionProb: cpmmContract.resolution
? cpmmContract.resolution
: Math.round(cpmmContract.prob * 100) + '%',
questionChange: questionChange:
(marketChange > 0 ? '+' : '') + (marketChange > 0 ? '+' : '') +
Math.round(marketChange * 100) + Math.round(marketChange * 100) +
@ -251,18 +229,31 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
// pick 3 winning investments and 3 losing investments // pick 3 winning investments and 3 losing investments
const topInvestments = winningInvestments.slice(0, 2) const topInvestments = winningInvestments.slice(0, 2)
const worstInvestments = losingInvestments.slice(0, 2) const worstInvestments = losingInvestments.slice(0, 2)
// console.log('winningInvestments', topInvestments) // if no bets in the last week ANd no market movers AND no markets created, don't send email
console.log('losingInvestments', worstInvestments) if (
log('perf data:', performanceData) contractsBetOnInLastWeek.length === 0 &&
topInvestments.length === 0 &&
worstInvestments.length === 0 &&
usersToContractsCreated[privateUser.id].length === 0
) {
log('No bets in last week, no market movers, no markets created')
await firestore.collection('private-users').doc(privateUser.id).update({
weeklyPortfolioUpdateEmailSent: true,
})
return
}
await sendWeeklyPortfolioUpdateEmail( await sendWeeklyPortfolioUpdateEmail(
user, user,
privateUser, privateUser,
topInvestments.concat(worstInvestments) as PerContractInvestmentsData[], topInvestments.concat(worstInvestments) as PerContractInvestmentsData[],
performanceData performanceData
) )
return firestore.collection('private-users').doc(privateUser.id).update({ await firestore.collection('private-users').doc(privateUser.id).update({
weeklyPortfolioUpdateEmailSent: true, weeklyPortfolioUpdateEmailSent: true,
}) })
log('Sent weekly portfolio update email to', privateUser.email)
count++
log('sent out emails to user count:', count)
}) })
) )
} }