Compare commits
No commits in common. "91e748525945aae759acdc44323076f00d39bc6a" and "c38d3fe30fdd2e5ec60cffac3adb6d073a96dba4" have entirely different histories.
91e7485259
...
c38d3fe30f
16
Memory.md
16
Memory.md
@ -141,12 +141,6 @@
|
||||
- 在 `src/actions/snapshots.ts` 中新增 `getEffectivePrice(assetId, targetDate)` 函数:在 `assetPricesHistory` 表中查询指定 `assetId` 且 `date <= targetDate` 的记录,按照 `date` 降序排列 (`desc`) 并 `.limit(1)` 取第一条,实现价格断点结转(Forward-Fill)逻辑——如果目标当天没有导入价格,系统自动抓取该资产在目标日期之前「最新」的一次有效价格。
|
||||
- 两个函数均使用 `Big.js` 进行高精度数值计算,为历史净值时光机功能提供底层数据支撑。
|
||||
|
||||
## 修复 Drizzle ORM 中 lte 操作符的语法调用错误 (Task 50d)
|
||||
- 修复 Drizzle ORM 中 lte 操作符的语法调用错误,从链式调用更正为标准纯函数导入。
|
||||
- 在 `src/actions/snapshots.ts` 的 `getHistoricalPositions` 函数中,将 `transactions.executedAt.lte(targetDate)` 替换为 `lte(transactions.executedAt, targetDate)`。
|
||||
- 同时修复 `getSnapshots` 函数中的 `portfolioSnapshots.date.lte(endDate)` 为 `lte(portfolioSnapshots.date, endDate)`。
|
||||
- `lte` 操作符已从文件顶部 `drizzle-orm` 引入,无需额外添加导入。
|
||||
|
||||
## 净值时光机主引擎 - Day-by-Day 循环遍历重建 (Task 50b)
|
||||
- 完成净值时光机主引擎,通过 Day-by-Day 循环遍历历史流水并结合断点结转价格,自动重建全量历史资产快照。
|
||||
- 在 `src/actions/snapshots.ts` 中新增 `reconstructPortfolioHistory()` 函数:查询 `transactions` 表找出最早的 `executedAt` 作为回溯起点,转换为 `Asia/Shanghai` 时区后以天为单位循环至今天。
|
||||
@ -159,12 +153,4 @@
|
||||
- 点击按钮后调用 `reconstructPortfolioHistory()` Server Action,启动 Day-by-Day 历史净值回溯引擎。
|
||||
- 集成 Sonner Toast 通知:点击时显示 `toast.loading('正在重构历史走势...')`,完成后显示 `toast.success('重构成功,已填充 N 天历史数据')`,并自动刷新 `snapshots` 状态以更新 AreaChart 走势图。
|
||||
- 按钮启用 `isPending` 防重复点击,重构期间显示"重构中..."并禁用按钮。
|
||||
- 打通历史净值回溯全链路:用户从 Dashboard 首页一键触发,底层引擎自动从最早交易日起逐天计算持仓与价格,填充 `portfolio_snapshots` 表,前端图表实时渲染历史波动曲线。
|
||||
|
||||
## 修复时光机引擎的变量泄漏与日期补零问题 (Task 51)
|
||||
- 修复了时光机循环引擎中的变量作用域泄漏导致错误复用 BTC 价格的 Bug,并标准化了 YYYY-MM-DD 日期补零逻辑以修复 SQL 字符串对比错误。
|
||||
- 在 `src/actions/snapshots.ts` 中新增 `formatDateString(date)` 辅助函数,使用 `padStart(2, '0')` 严格保证月份和日期补零,替代各处散落的 `toISOString().split('T')[0]` 调用。
|
||||
- 修复 `getHistoricalPositions` 和 `getEffectivePrice` 中的日期字符串生成逻辑:统一使用 `formatDateString(targetDate)`,确保 Drizzle 的 `lte` 查询中左右两边日期字符串格式一致(均为 `YYYY-MM-DD`),修复了 UTC 与本地时区混用导致的查询偏移。
|
||||
- 修复 `reconstructPortfolioHistory` 主循环:while 条件与 `dateStr` 生成均改用 `formatDateString(currentDate)`,确保与 `getTodayInShanghai()` 返回格式完全一致。
|
||||
- 在资产遍历循环中增加兜底防御逻辑:当 `getEffectivePrice` 返回 null 时,使用 `assetLatestPriceMap` 中缓存的 `latestPrice` 作为兜底价格,避免价格变量为 undefined 或沿用上一资产的值;同时修正了 `totalCostCny` 在 `priceStr` 为空时不应累加的 Bug。
|
||||
- 在 `allAssets` 查询中新增 `latestPrice` 字段,构建 `assetLatestPriceMap` 供兜底逻辑使用。
|
||||
- 打通历史净值回溯全链路:用户从 Dashboard 首页一键触发,底层引擎自动从最早交易日起逐天计算持仓与价格,填充 `portfolio_snapshots` 表,前端图表实时渲染历史波动曲线。
|
||||
@ -6,20 +6,16 @@ import { getPortfolioPositions } from './portfolio';
|
||||
import { asc, desc, eq, gte, lte, sql } from 'drizzle-orm';
|
||||
import Big from 'big.js';
|
||||
|
||||
function formatDateString(date: Date): string {
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
function getTodayInShanghai(): string {
|
||||
const now = new Date();
|
||||
const utcStr = now.toLocaleString('en-US', { timeZone: 'UTC' });
|
||||
const utcDate = new Date(utcStr);
|
||||
const shanghaiOffset = 8 * 60 * 60 * 1000;
|
||||
const shanghaiDate = new Date(utcDate.getTime() + shanghaiOffset);
|
||||
return formatDateString(shanghaiDate);
|
||||
const year = shanghaiDate.getFullYear();
|
||||
const month = String(shanghaiDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(shanghaiDate.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export async function recordDailySnapshot() {
|
||||
@ -101,7 +97,7 @@ export async function getSnapshots(params?: {
|
||||
}
|
||||
if (endDate) {
|
||||
query = query.where(
|
||||
lte(portfolioSnapshots.date, endDate)
|
||||
portfolioSnapshots.date.lte(endDate)
|
||||
);
|
||||
}
|
||||
|
||||
@ -117,7 +113,7 @@ interface HistoricalPosition {
|
||||
}
|
||||
|
||||
export async function getHistoricalPositions(targetDate: Date): Promise<HistoricalPosition[]> {
|
||||
const dateStr = formatDateString(targetDate);
|
||||
const dateStr = targetDate.toISOString().split('T')[0];
|
||||
|
||||
const allTransactions = await db
|
||||
.select({
|
||||
@ -129,8 +125,8 @@ export async function getHistoricalPositions(targetDate: Date): Promise<Historic
|
||||
executedAt: transactions.executedAt,
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
lte(transactions.executedAt, targetDate)
|
||||
.where(
|
||||
transactions.executedAt.lte(targetDate)
|
||||
)
|
||||
.orderBy(asc(transactions.executedAt));
|
||||
|
||||
@ -187,7 +183,7 @@ export async function getEffectivePrice(
|
||||
assetId: string,
|
||||
targetDate: Date
|
||||
): Promise<string | null> {
|
||||
const dateStr = formatDateString(targetDate);
|
||||
const dateStr = targetDate.toISOString().split('T')[0];
|
||||
|
||||
const [record] = await db
|
||||
.select({
|
||||
@ -231,14 +227,11 @@ export async function reconstructPortfolioHistory() {
|
||||
.select({
|
||||
id: assets.id,
|
||||
baseCurrency: assets.baseCurrency,
|
||||
latestPrice: assets.latestPrice,
|
||||
})
|
||||
.from(assets);
|
||||
const assetBaseCurrencyMap = new Map<string, string>();
|
||||
const assetLatestPriceMap = new Map<string, string>();
|
||||
for (const a of allAssets) {
|
||||
assetBaseCurrencyMap.set(a.id, a.baseCurrency);
|
||||
assetLatestPriceMap.set(a.id, a.latestPrice || '0');
|
||||
}
|
||||
|
||||
const allRates = await db
|
||||
@ -280,8 +273,8 @@ export async function reconstructPortfolioHistory() {
|
||||
|
||||
let daysReconstructed = 0;
|
||||
|
||||
while (formatDateString(currentDate) <= todayStr) {
|
||||
const dateStr = formatDateString(currentDate);
|
||||
while (currentDate.toISOString().split('T')[0] <= todayStr) {
|
||||
const dateStr = currentDate.toISOString().split('T')[0];
|
||||
|
||||
const positions = await getHistoricalPositions(currentDate);
|
||||
|
||||
@ -291,21 +284,12 @@ export async function reconstructPortfolioHistory() {
|
||||
for (const pos of positions) {
|
||||
const priceStr = await getEffectivePrice(pos.assetId, currentDate);
|
||||
const baseCurrency = assetBaseCurrencyMap.get(pos.assetId) || 'USD';
|
||||
|
||||
if (!priceStr) {
|
||||
const fallbackPrice = assetLatestPriceMap.get(pos.assetId) || '0';
|
||||
const cnyPrice = convertPriceToCny(fallbackPrice, baseCurrency);
|
||||
if (priceStr) {
|
||||
const cnyPrice = convertPriceToCny(priceStr, baseCurrency);
|
||||
const price = new Big(cnyPrice);
|
||||
const qty = new Big(pos.quantity);
|
||||
totalValueCny = totalValueCny.plus(price.times(qty));
|
||||
totalCostCny = totalCostCny.plus(pos.totalCost);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cnyPrice = convertPriceToCny(priceStr, baseCurrency);
|
||||
const price = new Big(cnyPrice);
|
||||
const qty = new Big(pos.quantity);
|
||||
totalValueCny = totalValueCny.plus(price.times(qty));
|
||||
totalCostCny = totalCostCny.plus(pos.totalCost);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user