diff --git a/app/components/TransactionHistoryModal.jsx b/app/components/TransactionHistoryModal.jsx index 0da767b..5e06b31 100644 --- a/app/components/TransactionHistoryModal.jsx +++ b/app/components/TransactionHistoryModal.jsx @@ -78,7 +78,7 @@ export default function TransactionHistoryModal({ onClick={onAddHistory} style={{ fontSize: '12px', padding: '4px 12px', height: 'auto' }} > - ➕ 添加记录 + 添加记录 diff --git a/app/lib/tradingCalendar.js b/app/lib/tradingCalendar.js new file mode 100644 index 0000000..4e5f741 --- /dev/null +++ b/app/lib/tradingCalendar.js @@ -0,0 +1,56 @@ +/** + * A股交易日历:基于 chinese-days 节假日数据,严格判断某日期是否为交易日 + * 交易日 = 周一至周五 且 不在法定节假日 + * 调休补班日(周末变工作日)A股仍休市,故不视为交易日 + */ + +const CDN_BASE = 'https://cdn.jsdelivr.net/npm/chinese-days@1/dist/years'; +const yearCache = new Map(); // year -> Set (holidays) + +/** + * 加载某年的节假日数据 + * @param {number} year + * @returns {Promise>} 节假日日期集合,格式 YYYY-MM-DD + */ +export async function loadHolidaysForYear(year) { + if (yearCache.has(year)) { + return yearCache.get(year); + } + try { + const res = await fetch(`${CDN_BASE}/${year}.json`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + const holidays = new Set(Object.keys(data?.holidays ?? {})); + yearCache.set(year, holidays); + return holidays; + } catch (e) { + console.warn(`[tradingCalendar] 加载 ${year} 年节假日失败:`, e); + yearCache.set(year, new Set()); + return yearCache.get(year); + } +} + +/** + * 加载多个年份的节假日数据 + * @param {number[]} years + */ +export async function loadHolidaysForYears(years) { + await Promise.all([...new Set(years)].map(loadHolidaysForYear)); +} + +/** + * 判断某日期是否为 A股交易日 + * @param {dayjs.Dayjs} date - dayjs 对象 + * @param {Map>} [cache] - 可选,已加载的年份缓存,默认使用内部 yearCache + * @returns {boolean} + */ +export function isTradingDay(date, cache = yearCache) { + const dayOfWeek = date.day(); // 0=周日, 6=周六 + if (dayOfWeek === 0 || dayOfWeek === 6) return false; + + const dateStr = date.format('YYYY-MM-DD'); + const year = date.year(); + const holidays = cache.get(year); + if (!holidays) return true; // 未加载该年数据时,仅排除周末 + return !holidays.has(dateStr); +} diff --git a/app/page.jsx b/app/page.jsx index 48cb15e..522276a 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -43,6 +43,7 @@ import DcaModal from "./components/DcaModal"; import githubImg from "./assets/github.svg"; import { supabase, isSupabaseConfigured } from './lib/supabase'; import { recordValuation, getAllValuationSeries, clearFund } from './lib/valuationTimeseries'; +import { loadHolidaysForYears, isTradingDay as isDateTradingDay } from './lib/tradingCalendar'; import { fetchFundData, fetchLatestRelease, fetchShanghaiIndexDate, fetchSmartFundNetValue, searchFunds, extractFundNamesWithLLM } from './api/fund'; import packageJson from '../package.json'; import PcFundTable from './components/PcFundTable'; @@ -1635,7 +1636,7 @@ export default function HomePage() { }); }; - const scheduleDcaTrades = useCallback(() => { + const scheduleDcaTrades = useCallback(async () => { if (!isTradingDay) return; if (!isPlainObject(dcaPlans)) return; const codesSet = new Set(funds.map((f) => f.code)); @@ -1645,6 +1646,14 @@ export default function HomePage() { const nextPlans = { ...dcaPlans }; const newPending = []; + // 预加载回溯区间内所有年份的节假日数据 + const years = new Set([today.year()]); + Object.values(dcaPlans).forEach((plan) => { + if (plan?.firstDate) years.add(toTz(plan.firstDate).year()); + if (plan?.lastDate) years.add(toTz(plan.lastDate).year()); + }); + await loadHolidaysForYears([...years]); + Object.entries(dcaPlans).forEach(([code, plan]) => { if (!plan || !plan.enabled) return; if (!codesSet.has(code)) return; @@ -1680,12 +1689,10 @@ export default function HomePage() { if (current.isAfter(today, 'day')) break; if (current.isBefore(first, 'day')) continue; + // 回溯补单:严格判断该日是否为 A股交易日(排除周末、法定节假日) + if (!isDateTradingDay(current)) continue; + const dateStr = current.format('YYYY-MM-DD'); - // 每日定投:跳过周末(周六、周日),只生成交易日 - if (cycle === 'daily') { - const dayOfWeek = current.day(); // 0=周日, 6=周六 - if (dayOfWeek === 0 || dayOfWeek === 6) continue; - } const pending = { id: `dca_${code}_${dateStr}_${Date.now()}`, @@ -1736,7 +1743,9 @@ export default function HomePage() { useEffect(() => { if (!isTradingDay) return; - scheduleDcaTrades(); + scheduleDcaTrades().catch((e) => { + console.error('[scheduleDcaTrades]', e); + }); }, [isTradingDay, scheduleDcaTrades]); const handleAddGroup = (name) => {