68 lines
2.3 KiB
JavaScript
68 lines
2.3 KiB
JavaScript
|
|
// 公司研究同步節奏:首次進入必抓;之後等到「下次財報/公開」再更新
|
|||
|
|
import { fetchEarningsEvents } from './calendar.js';
|
|||
|
|
|
|||
|
|
function addDaysISO(base, days) {
|
|||
|
|
const d = new Date(base + 'T12:00:00Z');
|
|||
|
|
d.setUTCDate(d.getUTCDate() + days);
|
|||
|
|
return d.toISOString().slice(0, 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function todayISO() {
|
|||
|
|
return new Date().toISOString().slice(0, 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 下次允許重新抓取的日期 = 下一個財報日(尚無則約一季後) */
|
|||
|
|
export async function computeNextPublicRefresh(symbol) {
|
|||
|
|
symbol = String(symbol || '').trim().toUpperCase();
|
|||
|
|
const today = todayISO();
|
|||
|
|
const end = addDaysISO(today, 200);
|
|||
|
|
try {
|
|||
|
|
const events = await fetchEarningsEvents(today, end, [symbol]);
|
|||
|
|
const upcoming = (events || [])
|
|||
|
|
.filter(e => e.date && e.date > today)
|
|||
|
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|||
|
|
if (upcoming.length) {
|
|||
|
|
const next = upcoming[0];
|
|||
|
|
return {
|
|||
|
|
nextRefreshAfter: next.date,
|
|||
|
|
nextPublicLabel: next.title || `${symbol} 財報`,
|
|||
|
|
nextPublicDate: next.date,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
} catch { /* fallback */ }
|
|||
|
|
const fallback = addDaysISO(today, 92);
|
|||
|
|
return {
|
|||
|
|
nextRefreshAfter: fallback,
|
|||
|
|
nextPublicLabel: '約一季後(暫無財報日曆)',
|
|||
|
|
nextPublicDate: fallback,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function intelRefreshPolicy(enrichedRow) {
|
|||
|
|
const data = enrichedRow?.data || {};
|
|||
|
|
const lastSyncAt = data.lastSyncAt || enrichedRow?.updatedAt || null;
|
|||
|
|
const nextRefreshAfter = data.nextRefreshAfter || null;
|
|||
|
|
const today = todayISO();
|
|||
|
|
const neverSynced = !lastSyncAt;
|
|||
|
|
const due = nextRefreshAfter ? today >= nextRefreshAfter : false;
|
|||
|
|
const needsSync = neverSynced || due;
|
|||
|
|
let skipReason = null;
|
|||
|
|
if (!needsSync && nextRefreshAfter) {
|
|||
|
|
skipReason = `已同步;下次更新:${nextRefreshAfter}(${data.nextPublicLabel || '下次財報/公開'})`;
|
|||
|
|
} else if (!needsSync) {
|
|||
|
|
skipReason = '已同步';
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
needsSync,
|
|||
|
|
lastSyncAt: lastSyncAt ? new Date(lastSyncAt).toISOString() : null,
|
|||
|
|
nextRefreshAfter,
|
|||
|
|
nextPublicLabel: data.nextPublicLabel || null,
|
|||
|
|
skipReason,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function shouldRunIntelSync(enrichedRow, { force = false } = {}) {
|
|||
|
|
if (force) return { run: true, reason: 'force' };
|
|||
|
|
const p = intelRefreshPolicy(enrichedRow);
|
|||
|
|
return { run: p.needsSync, reason: p.needsSync ? 'first_or_due' : 'wait_public', ...p };
|
|||
|
|
}
|