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 };
|
||
} |