fix:定投触发需严格判断是否为交易日
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
56
app/lib/tradingCalendar.js
Normal file
56
app/lib/tradingCalendar.js
Normal 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);
|
||||||
|
}
|
||||||
23
app/page.jsx
23
app/page.jsx
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user