fix:定投触发需严格判断是否为交易日

This commit is contained in:
hzm
2026-03-02 08:44:23 +08:00
parent 3958580571
commit 39f8152e70
3 changed files with 73 additions and 8 deletions

View File

@@ -78,7 +78,7 @@ export default function TransactionHistoryModal({
onClick={onAddHistory} onClick={onAddHistory}
style={{ fontSize: '12px', padding: '4px 12px', height: 'auto' }} style={{ fontSize: '12px', padding: '4px 12px', height: 'auto' }}
> >
添加记录 添加记录
</button> </button>
</div> </div>

View File

@@ -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<dateStr> (holidays)
/**
* 加载某年的节假日数据
* @param {number} year
* @returns {Promise<Set<string>>} 节假日日期集合,格式 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<number, Set<string>>} [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);
}

View File

@@ -43,6 +43,7 @@ import DcaModal from "./components/DcaModal";
import githubImg from "./assets/github.svg"; import githubImg from "./assets/github.svg";
import { supabase, isSupabaseConfigured } from './lib/supabase'; import { supabase, isSupabaseConfigured } from './lib/supabase';
import { recordValuation, getAllValuationSeries, clearFund } from './lib/valuationTimeseries'; 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 { fetchFundData, fetchLatestRelease, fetchShanghaiIndexDate, fetchSmartFundNetValue, searchFunds, extractFundNamesWithLLM } from './api/fund';
import packageJson from '../package.json'; import packageJson from '../package.json';
import PcFundTable from './components/PcFundTable'; import PcFundTable from './components/PcFundTable';
@@ -1635,7 +1636,7 @@ export default function HomePage() {
}); });
}; };
const scheduleDcaTrades = useCallback(() => { const scheduleDcaTrades = useCallback(async () => {
if (!isTradingDay) return; if (!isTradingDay) return;
if (!isPlainObject(dcaPlans)) return; if (!isPlainObject(dcaPlans)) return;
const codesSet = new Set(funds.map((f) => f.code)); const codesSet = new Set(funds.map((f) => f.code));
@@ -1645,6 +1646,14 @@ export default function HomePage() {
const nextPlans = { ...dcaPlans }; const nextPlans = { ...dcaPlans };
const newPending = []; 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]) => { Object.entries(dcaPlans).forEach(([code, plan]) => {
if (!plan || !plan.enabled) return; if (!plan || !plan.enabled) return;
if (!codesSet.has(code)) return; if (!codesSet.has(code)) return;
@@ -1680,12 +1689,10 @@ export default function HomePage() {
if (current.isAfter(today, 'day')) break; if (current.isAfter(today, 'day')) break;
if (current.isBefore(first, 'day')) continue; if (current.isBefore(first, 'day')) continue;
// 回溯补单:严格判断该日是否为 A股交易日排除周末、法定节假日
if (!isDateTradingDay(current)) continue;
const dateStr = current.format('YYYY-MM-DD'); const dateStr = current.format('YYYY-MM-DD');
// 每日定投:跳过周末(周六、周日),只生成交易日
if (cycle === 'daily') {
const dayOfWeek = current.day(); // 0=周日, 6=周六
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
}
const pending = { const pending = {
id: `dca_${code}_${dateStr}_${Date.now()}`, id: `dca_${code}_${dateStr}_${Date.now()}`,
@@ -1736,7 +1743,9 @@ export default function HomePage() {
useEffect(() => { useEffect(() => {
if (!isTradingDay) return; if (!isTradingDay) return;
scheduleDcaTrades(); scheduleDcaTrades().catch((e) => {
console.error('[scheduleDcaTrades]', e);
});
}, [isTradingDay, scheduleDcaTrades]); }, [isTradingDay, scheduleDcaTrades]);
const handleAddGroup = (name) => { const handleAddGroup = (name) => {