|
|
@ -1,5 +1,11 @@ |
|
|
import { PrismaPg } from '@prisma/adapter-pg'; |
|
|
import { PrismaPg } from '@prisma/adapter-pg'; |
|
|
import { PrismaClient } from '@prisma/client'; |
|
|
import { |
|
|
|
|
|
AssetClass, |
|
|
|
|
|
AssetSubClass, |
|
|
|
|
|
DataSource, |
|
|
|
|
|
PrismaClient, |
|
|
|
|
|
Type |
|
|
|
|
|
} from '@prisma/client'; |
|
|
|
|
|
|
|
|
const adapter = new PrismaPg({ |
|
|
const adapter = new PrismaPg({ |
|
|
connectionString: process.env.DIRECT_URL ?? process.env.DATABASE_URL |
|
|
connectionString: process.env.DIRECT_URL ?? process.env.DATABASE_URL |
|
|
@ -7,7 +13,61 @@ const adapter = new PrismaPg({ |
|
|
|
|
|
|
|
|
const prisma = new PrismaClient({ adapter }); |
|
|
const prisma = new PrismaClient({ adapter }); |
|
|
|
|
|
|
|
|
async function main() { |
|
|
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; |
|
|
|
|
|
const PRICE_HISTORY_DAYS = 730; |
|
|
|
|
|
|
|
|
|
|
|
function createSeededRandom(seed: number) { |
|
|
|
|
|
return () => { |
|
|
|
|
|
seed = (seed * 9301 + 49297) % 233280; |
|
|
|
|
|
return seed / 233280; |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getToday() { |
|
|
|
|
|
const today = new Date(); |
|
|
|
|
|
today.setUTCHours(0, 0, 0, 0); |
|
|
|
|
|
return today; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getDateDaysAgo(daysAgo: number) { |
|
|
|
|
|
return new Date(getToday().getTime() - daysAgo * ONE_DAY_IN_MS); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function generatePriceHistory({ |
|
|
|
|
|
drift, |
|
|
|
|
|
seed, |
|
|
|
|
|
startPrice, |
|
|
|
|
|
volatility |
|
|
|
|
|
}: { |
|
|
|
|
|
drift: number; |
|
|
|
|
|
seed: number; |
|
|
|
|
|
startPrice: number; |
|
|
|
|
|
volatility: number; |
|
|
|
|
|
}) { |
|
|
|
|
|
const random = createSeededRandom(seed); |
|
|
|
|
|
let price = startPrice; |
|
|
|
|
|
const prices: { date: Date; price: number }[] = []; |
|
|
|
|
|
|
|
|
|
|
|
for (let daysAgo = PRICE_HISTORY_DAYS; daysAgo >= 0; daysAgo--) { |
|
|
|
|
|
price = Math.max(price * (1 + drift + (random() - 0.5) * volatility), 0.01); |
|
|
|
|
|
prices.push({ |
|
|
|
|
|
date: getDateDaysAgo(daysAgo), |
|
|
|
|
|
price: Math.round(price * 100) / 100 |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return prices; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getPriceDaysAgo( |
|
|
|
|
|
priceHistory: { date: Date; price: number }[], |
|
|
|
|
|
daysAgo: number |
|
|
|
|
|
) { |
|
|
|
|
|
const index = priceHistory.length - 1 - daysAgo; |
|
|
|
|
|
return priceHistory[Math.max(index, 0)].price; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function seedTags() { |
|
|
await prisma.tag.createMany({ |
|
|
await prisma.tag.createMany({ |
|
|
data: [ |
|
|
data: [ |
|
|
{ |
|
|
{ |
|
|
@ -23,6 +83,361 @@ async function main() { |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function seedPortfolio() { |
|
|
|
|
|
const user = await prisma.user.findFirst({ orderBy: { createdAt: 'asc' } }); |
|
|
|
|
|
|
|
|
|
|
|
if (!user) { |
|
|
|
|
|
console.warn( |
|
|
|
|
|
'No user found. Sign in once to create a user, then run the seed again to add sample portfolio data.' |
|
|
|
|
|
); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const platformBrokerage = await prisma.platform.upsert({ |
|
|
|
|
|
create: { |
|
|
|
|
|
id: '9fe1a2ce-b5b3-41c7-a61f-73d6c8c346f5', |
|
|
|
|
|
name: 'Interactive Brokers', |
|
|
|
|
|
url: 'https://www.interactivebrokers.com' |
|
|
|
|
|
}, |
|
|
|
|
|
update: {}, |
|
|
|
|
|
where: { url: 'https://www.interactivebrokers.com' } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const platformCrypto = await prisma.platform.upsert({ |
|
|
|
|
|
create: { |
|
|
|
|
|
id: '1ef9a79a-61e6-4499-89eb-74b665cc8261', |
|
|
|
|
|
name: 'Binance', |
|
|
|
|
|
url: 'https://www.binance.com' |
|
|
|
|
|
}, |
|
|
|
|
|
update: {}, |
|
|
|
|
|
where: { url: 'https://www.binance.com' } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const accountBrokerageId = '6798f55e-b2b5-40ef-8270-7f245e4da4d1'; |
|
|
|
|
|
const accountCryptoId = '0c660823-a36f-4903-9f24-71c6211399d8'; |
|
|
|
|
|
|
|
|
|
|
|
await prisma.account.upsert({ |
|
|
|
|
|
create: { |
|
|
|
|
|
id: accountBrokerageId, |
|
|
|
|
|
balance: 2500, |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
name: 'Brokerage Account', |
|
|
|
|
|
platformId: platformBrokerage.id, |
|
|
|
|
|
userId: user.id |
|
|
|
|
|
}, |
|
|
|
|
|
update: {}, |
|
|
|
|
|
where: { id_userId: { id: accountBrokerageId, userId: user.id } } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
await prisma.account.upsert({ |
|
|
|
|
|
create: { |
|
|
|
|
|
id: accountCryptoId, |
|
|
|
|
|
balance: 500, |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
name: 'Crypto Exchange', |
|
|
|
|
|
platformId: platformCrypto.id, |
|
|
|
|
|
userId: user.id |
|
|
|
|
|
}, |
|
|
|
|
|
update: {}, |
|
|
|
|
|
where: { id_userId: { id: accountCryptoId, userId: user.id } } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const symbols = { |
|
|
|
|
|
AAPL: { |
|
|
|
|
|
id: 'd7ae0976-9f0e-4a38-81a3-7cec78806436', |
|
|
|
|
|
symbol: 'AAPL.SEED', |
|
|
|
|
|
name: 'Apple Inc.', |
|
|
|
|
|
assetClass: AssetClass.EQUITY, |
|
|
|
|
|
assetSubClass: AssetSubClass.STOCK, |
|
|
|
|
|
priceHistory: generatePriceHistory({ |
|
|
|
|
|
drift: 0.0006, |
|
|
|
|
|
seed: 1, |
|
|
|
|
|
startPrice: 150, |
|
|
|
|
|
volatility: 0.02 |
|
|
|
|
|
}) |
|
|
|
|
|
}, |
|
|
|
|
|
MSFT: { |
|
|
|
|
|
id: '4c84f62e-20c5-4d5f-8d51-63221dc64c8a', |
|
|
|
|
|
symbol: 'MSFT.SEED', |
|
|
|
|
|
name: 'Microsoft Corporation', |
|
|
|
|
|
assetClass: AssetClass.EQUITY, |
|
|
|
|
|
assetSubClass: AssetSubClass.STOCK, |
|
|
|
|
|
priceHistory: generatePriceHistory({ |
|
|
|
|
|
drift: 0.0007, |
|
|
|
|
|
seed: 2, |
|
|
|
|
|
startPrice: 300, |
|
|
|
|
|
volatility: 0.018 |
|
|
|
|
|
}) |
|
|
|
|
|
}, |
|
|
|
|
|
VTI: { |
|
|
|
|
|
id: 'bc960e73-2ed5-4f46-82df-2a86595148e5', |
|
|
|
|
|
symbol: 'VTI.SEED', |
|
|
|
|
|
name: 'Vanguard Total Stock Market ETF', |
|
|
|
|
|
assetClass: AssetClass.EQUITY, |
|
|
|
|
|
assetSubClass: AssetSubClass.ETF, |
|
|
|
|
|
priceHistory: generatePriceHistory({ |
|
|
|
|
|
drift: 0.0004, |
|
|
|
|
|
seed: 3, |
|
|
|
|
|
startPrice: 220, |
|
|
|
|
|
volatility: 0.01 |
|
|
|
|
|
}) |
|
|
|
|
|
}, |
|
|
|
|
|
BTC: { |
|
|
|
|
|
id: 'f55270af-eb6c-4328-8411-71bf08f4803a', |
|
|
|
|
|
symbol: 'BTC.SEED', |
|
|
|
|
|
name: 'Bitcoin', |
|
|
|
|
|
assetClass: AssetClass.LIQUIDITY, |
|
|
|
|
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY, |
|
|
|
|
|
priceHistory: generatePriceHistory({ |
|
|
|
|
|
drift: 0.001, |
|
|
|
|
|
seed: 4, |
|
|
|
|
|
startPrice: 28000, |
|
|
|
|
|
volatility: 0.04 |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
} as const; |
|
|
|
|
|
|
|
|
|
|
|
const custodyFeeProfileId = 'e9cec288-3486-4f12-9dc5-9a57f3883b43'; |
|
|
|
|
|
const custodyFeeSymbol = '2160cb24-5906-499e-b854-9c7278cb35b1'; |
|
|
|
|
|
|
|
|
|
|
|
for (const { |
|
|
|
|
|
id, |
|
|
|
|
|
symbol, |
|
|
|
|
|
name, |
|
|
|
|
|
assetClass, |
|
|
|
|
|
assetSubClass, |
|
|
|
|
|
priceHistory |
|
|
|
|
|
} of Object.values(symbols)) { |
|
|
|
|
|
await prisma.symbolProfile.upsert({ |
|
|
|
|
|
create: { |
|
|
|
|
|
id, |
|
|
|
|
|
symbol, |
|
|
|
|
|
assetClass, |
|
|
|
|
|
assetSubClass, |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
name, |
|
|
|
|
|
userId: user.id |
|
|
|
|
|
}, |
|
|
|
|
|
update: {}, |
|
|
|
|
|
where: { dataSource_symbol: { dataSource: DataSource.MANUAL, symbol } } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
await prisma.marketData.createMany({ |
|
|
|
|
|
data: priceHistory.map(({ date, price }) => { |
|
|
|
|
|
return { |
|
|
|
|
|
date, |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
marketPrice: price, |
|
|
|
|
|
symbol |
|
|
|
|
|
}; |
|
|
|
|
|
}), |
|
|
|
|
|
skipDuplicates: true |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await prisma.symbolProfile.upsert({ |
|
|
|
|
|
create: { |
|
|
|
|
|
id: custodyFeeProfileId, |
|
|
|
|
|
symbol: custodyFeeSymbol, |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
name: 'Custody Fee', |
|
|
|
|
|
userId: user.id |
|
|
|
|
|
}, |
|
|
|
|
|
update: {}, |
|
|
|
|
|
where: { |
|
|
|
|
|
dataSource_symbol: { |
|
|
|
|
|
dataSource: DataSource.MANUAL, |
|
|
|
|
|
symbol: custodyFeeSymbol |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const orders: { |
|
|
|
|
|
id: string; |
|
|
|
|
|
accountId: string; |
|
|
|
|
|
symbolProfileId: string; |
|
|
|
|
|
type: Type; |
|
|
|
|
|
daysAgo: number; |
|
|
|
|
|
quantity: number; |
|
|
|
|
|
fee: number; |
|
|
|
|
|
unitPrice?: number; |
|
|
|
|
|
}[] = [ |
|
|
|
|
|
{ |
|
|
|
|
|
id: 'bbe70675-1ff7-48e2-9af3-99e2c9bf01ba', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.AAPL.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 700, |
|
|
|
|
|
quantity: 20, |
|
|
|
|
|
fee: 4.95 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '15aa3ddc-4212-4a54-83fa-f2fad8791dfd', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.MSFT.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 650, |
|
|
|
|
|
quantity: 10, |
|
|
|
|
|
fee: 4.95 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '7a7c2607-20a2-4f00-a0ff-7ae2f85f2ff3', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.VTI.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 600, |
|
|
|
|
|
quantity: 15, |
|
|
|
|
|
fee: 0 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '46c66fe3-f12c-40d1-bb6d-8e1eee9b115f', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.AAPL.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 400, |
|
|
|
|
|
quantity: 10, |
|
|
|
|
|
fee: 4.95 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '5f835cc2-f7da-481c-8043-7011074f5045', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.VTI.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 300, |
|
|
|
|
|
quantity: 15, |
|
|
|
|
|
fee: 0 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '98d0d61c-ab30-49d1-b57c-629930a140bf', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.MSFT.id, |
|
|
|
|
|
type: Type.DIVIDEND, |
|
|
|
|
|
daysAgo: 200, |
|
|
|
|
|
quantity: 1, |
|
|
|
|
|
fee: 0, |
|
|
|
|
|
unitPrice: 18.5 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: 'ce733963-672a-4569-880a-7fc92de8baa5', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.MSFT.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 150, |
|
|
|
|
|
quantity: 5, |
|
|
|
|
|
fee: 4.95 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: 'f82b04dd-ab56-4061-a67e-2fabd0303068', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.AAPL.id, |
|
|
|
|
|
type: Type.DIVIDEND, |
|
|
|
|
|
daysAgo: 100, |
|
|
|
|
|
quantity: 1, |
|
|
|
|
|
fee: 0, |
|
|
|
|
|
unitPrice: 9.6 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '1b49f226-89bf-41e2-8613-3446e3722b34', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: custodyFeeProfileId, |
|
|
|
|
|
type: Type.FEE, |
|
|
|
|
|
daysAgo: 90, |
|
|
|
|
|
quantity: 1, |
|
|
|
|
|
fee: 0, |
|
|
|
|
|
unitPrice: 5 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: 'ca5ef988-3595-4d76-b9b1-19541ebf4fad', |
|
|
|
|
|
accountId: accountBrokerageId, |
|
|
|
|
|
symbolProfileId: symbols.AAPL.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 50, |
|
|
|
|
|
quantity: 5, |
|
|
|
|
|
fee: 4.95 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '8352549c-68a0-4c9f-b4d3-910970219c0b', |
|
|
|
|
|
accountId: accountCryptoId, |
|
|
|
|
|
symbolProfileId: symbols.BTC.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 500, |
|
|
|
|
|
quantity: 0.05, |
|
|
|
|
|
fee: 10 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: 'c8f9bcda-215e-4a63-af43-a47de6517fcf', |
|
|
|
|
|
accountId: accountCryptoId, |
|
|
|
|
|
symbolProfileId: symbols.BTC.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 300, |
|
|
|
|
|
quantity: 0.03, |
|
|
|
|
|
fee: 8 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '75dfaa3d-e73d-4a73-a2ef-d5cc98243b4f', |
|
|
|
|
|
accountId: accountCryptoId, |
|
|
|
|
|
symbolProfileId: symbols.BTC.id, |
|
|
|
|
|
type: Type.BUY, |
|
|
|
|
|
daysAgo: 100, |
|
|
|
|
|
quantity: 0.02, |
|
|
|
|
|
fee: 5 |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
id: '7adf12c3-839c-4a0d-872d-13083a9b84b7', |
|
|
|
|
|
accountId: accountCryptoId, |
|
|
|
|
|
symbolProfileId: symbols.BTC.id, |
|
|
|
|
|
type: Type.SELL, |
|
|
|
|
|
daysAgo: 30, |
|
|
|
|
|
quantity: 0.01, |
|
|
|
|
|
fee: 5 |
|
|
|
|
|
} |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
const priceHistoryBySymbolProfileId = Object.fromEntries( |
|
|
|
|
|
Object.values(symbols).map(({ id, priceHistory }) => [id, priceHistory]) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
for (const order of orders) { |
|
|
|
|
|
const unitPrice = |
|
|
|
|
|
order.unitPrice ?? |
|
|
|
|
|
getPriceDaysAgo( |
|
|
|
|
|
priceHistoryBySymbolProfileId[order.symbolProfileId], |
|
|
|
|
|
order.daysAgo |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const data = { |
|
|
|
|
|
accountId: order.accountId, |
|
|
|
|
|
accountUserId: user.id, |
|
|
|
|
|
currency: 'USD', |
|
|
|
|
|
date: getDateDaysAgo(order.daysAgo), |
|
|
|
|
|
fee: order.fee, |
|
|
|
|
|
isDraft: false, |
|
|
|
|
|
quantity: order.quantity, |
|
|
|
|
|
symbolProfileId: order.symbolProfileId, |
|
|
|
|
|
type: order.type, |
|
|
|
|
|
unitPrice, |
|
|
|
|
|
userId: user.id |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
await prisma.order.upsert({ |
|
|
|
|
|
create: { id: order.id, ...data }, |
|
|
|
|
|
update: data, |
|
|
|
|
|
where: { id: order.id } |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log( |
|
|
|
|
|
`Seeded sample portfolio data for user ${user.id} (2 accounts, ${Object.keys(symbols).length} symbols, ${orders.length} activities).` |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function main() { |
|
|
|
|
|
await seedTags(); |
|
|
|
|
|
await seedPortfolio(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
main() |
|
|
main() |
|
|
.catch((e) => { |
|
|
.catch((e) => { |
|
|
console.error(e); |
|
|
console.error(e); |
|
|
|