finance-tools/scripts/build-patterns-catalog.mjs

298 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ═══════════════════════════════════════════════════════════
// build-patterns-catalog.mjs
// 從《短線交易日線圖大全》目錄 + 72 頁 MD 產生完整 catalog.json
// 用法cd app && npm run build:patterns
// ═══════════════════════════════════════════════════════════
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
findPageForEntry,
loadAllPages,
norm,
resolvePatternContentDir,
} from '../lib/pattern-page-index.js';
import { supplementForCatalogName } from '../lib/pattern-supplements.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BOOK_DIR = resolvePatternContentDir();
const OUT_PATH = path.resolve(__dirname, '..', 'data', 'patterns', 'catalog.json');
const OVERRIDES_PATH = path.resolve(__dirname, '..', 'data', 'patterns', 'automation-overrides.json');
if (!BOOK_DIR) {
console.error('找不到線型教材資料夾(短線交易日線圖大全 或 content/raw/patterns');
process.exit(1);
}
const overrides = JSON.parse(fs.readFileSync(OVERRIDES_PATH, 'utf8'));
const CHAPTER_NAMES = {
1: '短線交易模式與交易系統',
2: '構成股價走勢圖的要素',
3: '當沖交易圖表型態',
4: '波段交易圖表型態',
5: '超短線交易圖表型態',
6: '大盤觀察與選股',
0: '附錄與心態',
};
const MODE_SLUG_SUFFIX = { '①': '_1', '②': '_2', '③': '_3', '④': '_4' };
function slugId(name, category, chapter) {
const modeMark = String(name || '').match(/[①②③④]/)?.[0] || '';
const modeSuffix = MODE_SLUG_SUFFIX[modeMark] || '';
const base = norm(name).slice(0, 28) || norm(category).slice(0, 12);
const slug = `p${chapter}_${base}${modeSuffix}`
.replace(/[^\w\u4e00-\u9fff]/g, '_')
.replace(/_+/g, '_')
.replace(/_$/g, '');
return slug.slice(0, 52);
}
function inferBias(text, name) {
const t = `${text} ${name}`;
if (/賣出|下跌|空方|下殺|偏空|超買|死亡|跌停|黑三兵|空方|減碼|摜破|風險|小心|勿追/.test(t)) return 'bearish';
if (/買進|上漲|多方|起漲|偏多|超賣|黃金|漲停|紅三兵|突破|反彈|續強|買點/.test(t)) return 'bullish';
return 'neutral';
}
function inferTimeframe(chapter, category) {
if (chapter === 5 || /超短線|跳動點|VWAP|10檔/.test(category)) return ['分', '日'];
if (chapter === 4 || /波段|布林|一目|斐波|酒田/.test(category)) return ['日', '週'];
if (chapter === 3 || /當沖|交易時段|9點/.test(category)) return ['日', '分'];
if (chapter === 1 || chapter === 2) return ['日'];
return ['日'];
}
function parseToc() {
const raw = fs.readFileSync(path.join(BOOK_DIR, '003.md'), 'utf8');
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
const entries = [];
let chapter = 0;
let chapterTitle = '';
let section = 'toc';
const skipExact = new Set([
'交易模式', '時間意識', '未必等於', '10檔報價', '下單方式', '逆限價單', 'OCO單', 'IFD單', 'IFDOCO單',
'構成要素', '基本要素', 'K線', '時間軸', '交易時段', '技術指標', '移動平均線', '葛蘭碧法則',
'複數均線', '隨機指標', '騰落指標', '相對強弱指標', 'MACD', '複數指標', '道氏理論', 'K線組成的型態',
'天花板.地板', '橫盤整理', '三角收斂型態', '旗形型態', '箱型整理', '分價量表', '當日現金交割',
'特定的股價波動', '反彈的時機點', '練習問題①', '練習問題②', '基本操作策略', '避免在連假期間持股',
'布林通道', '趨勢通道', '顧比均線', '一目均衡表', '基本結構', '三役好轉', '延遲線的用法',
'斐波那契回撤', 'SAR拋物線指標', 'ENV包絡線', '不同時間軸的均線', '歷史動率', 'HV歷史波動率',
'動向指標', 'DMI動向指標', 'PSY心理線', 'DMA指標', '酒田五法', '島狀反轉', '菱形頂型態', '杯柄型態',
'海龜交易法', 'K線種類', 'K線型態', 'K線組合', '帶量上漲的大陽線', '上下影線', '跳空缺口', '連續K線',
'壓力線.支撐線', '突破交易', '心理關卡', '摜破大關', '多空趨勢', '哪一種趨勢', 'VWAP', '長期趨勢',
'大盤指數', '美股收盤', '歷史數據', '最近兩週', '新股上市', '變更交易市場', '股票下市', '宣布下市',
'發布財報', '買賣越活躍', '股價漲幅排行', '尋找買賣點', '當沖交易', '波段交易', '超短線交易',
'買進模式①', '買進模式②', '買進模式③', '買進模式④', '賣出模式①', '賣出模式②', '賣出模式③', '賣出模式④',
'道氏理論①', '道氏理論②', '道氏理論③', '9點11點', '收盤前30分', '長短參數', '移動平均乖離率',
'黃金交叉', '死亡交叉', '多頭排列', '紅三兵', '黑三兵', '下影陽線', '下影陰線', '上影陽線', '上影陰線',
'覆蓋線', '切入線', '穿透線', '環抱線', '孕育線', '大陽線', '大陰線', '同時線', '漲停', '跌停',
]);
for (const line of lines) {
if (line === '圖表型態一覽表' || line === '本書的頁面構成') continue;
const ch = line.match(/^第([1-6])章 (.+)$/);
if (ch) {
chapter = Number(ch[1]);
chapterTitle = ch[2];
continue;
}
if (line.startsWith('末章 ')) {
chapter = 0;
chapterTitle = '投資心態';
entries.push({ chapter, chapterTitle, category: '末章', name: line.replace(/^末章 /, ''), fullTitle: line });
continue;
}
if (line.startsWith('專欄 ')) {
entries.push({ chapter, chapterTitle, category: '專欄', name: line.replace(/^專欄 /, ''), fullTitle: line });
continue;
}
const m = line.match(/^(.+?) (.+)$/);
if (!m) continue;
const category = m[1].trim();
const title = m[2].trim();
if (title.length < 4) continue;
if (skipExact.has(category) && category === title) continue;
if (category === title) continue;
entries.push({ chapter, chapterTitle, category, name: title, fullTitle: `${category} ${title}` });
}
return entries;
}
const PAGE_OVERRIDES = {
'黃金交叉': 20, '死亡交叉': 20, '多頭排列': 21,
'RSI相對強弱指標': 22, 'MACD指標': 23, 'MACD': 23,
'成交量': 29, '向上跳空缺口': 55, '向下跳空缺口': 55,
'多方吞噬': 54, '空方吞噬': 54, '覆蓋線': 54,
'下影陽線': 53, '下影陰線': 53, '上影陽線': 53, '上影陰線': 53,
'紅三兵': 45, '黑三兵': 45, '漲停': 10, '跌停': 10,
'買進模式①': 15, '買進模式④': 16,
'賣出模式①': 17, '賣出模式②': 18,
'跳動點': 50, 'VWAP': 59,
// 目錄有條目但原始擷取未產生獨立書頁
'10分鐘掌握當沖交易的重點': false,
'10分鐘掌握波段交易的重點': false,
'運用葛蘭碧法則的賣出模式③': false,
'運用葛蘭碧法則的賣出模式④': false,
'反轉為上升趨勢的關鍵點位在哪裡?①': false,
};
function resolveByNameOverride(entry) {
if (overrides.byName?.[entry.name]) return overrides.byName[entry.name];
const keys = Object.keys(overrides.byName || {}).sort((a, b) => b.length - a.length);
for (const key of keys) {
if (entry.name === key) return overrides.byName[key];
}
return {};
}
function applyOverride(entry, page) {
const split = overrides.byTitle?.[entry.name]?.split;
if (split?.length) {
return split.map(subName => {
const sub = { ...entry, name: subName };
const o = overrides.byName?.[subName] || {};
return buildPattern(sub, page, o);
});
}
return [buildPattern(entry, page, resolveByNameOverride(entry))];
}
function resolvePageForEntry(entry, pages) {
return supplementForCatalogName(entry.name) || findPageForEntry(entry, pages, PAGE_OVERRIDES);
}
function buildPattern(entry, page, o = {}) {
const summary = page?.summary || entry.name;
const bias = o.bias || inferBias(summary, entry.name);
const chapter = entry.chapter || 0;
const pattern = {
id: o.id || slugId(entry.name, entry.category, chapter),
name: o.name || entry.name,
category: entry.category,
chapter,
chapterTitle: entry.chapterTitle || CHAPTER_NAMES[chapter] || '',
bookPage: page?.pageNum || null,
bias,
timeframe: o.timeframe || inferTimeframe(chapter, entry.category),
automatable: o.automatable === true,
teach: {
summary: summary.slice(0, 280),
entryHint: o.entryHint || (page?.summary2 ? page.summary2.slice(0, 120) : '依書本圖例與當下趨勢判斷進場時機。'),
exitHint: o.exitHint || '趨勢或型態失效時出場;搭配停損/停利紀律。',
caution: page?.isSupplement
? '此節為教材補遺:原始擷取缺頁,內容依書中交叉引用整理,圖例可能為鏡像對照頁。'
: (o.caution || (entry.category === '練習問題' ? '先自行作答,再對照書本解答。' : '訊號需搭配大盤、量能與基本面,避免單一線型決策。')),
bookRef: page?.mdFile || null,
bookHtml: page?.htmlFile || null,
images: page?.imageUrls || [],
fullTitle: entry.fullTitle,
isSupplement: !!page?.isSupplement,
tocHint: page?.tocHint || null,
},
};
if (o.rule) {
pattern.rule = o.rule;
pattern.params = o.params || {};
}
if (o.markets) pattern.markets = o.markets;
if (o.backtest) pattern.backtest = o.backtest;
const extra = overrides.teachExtras?.[pattern.id];
if (extra) {
const { teach: extraTeach, entryHint, exitHint, caution, ...rest } = extra;
Object.assign(pattern, rest);
if (extraTeach) pattern.teach = { ...pattern.teach, ...extraTeach };
if (entryHint) pattern.teach.entryHint = entryHint;
if (exitHint) pattern.teach.exitHint = exitHint;
if (caution) pattern.teach.caution = caution;
}
return pattern;
}
function dedupePatterns(list) {
const seen = new Map();
const out = [];
for (const p of list) {
const key = p.id;
if (seen.has(key)) {
const prev = seen.get(key);
if ((p.automatable && !prev.automatable) || (p.bookPage && !prev.bookPage)) {
const idx = out.indexOf(prev);
out[idx] = p;
seen.set(key, p);
}
continue;
}
seen.set(key, p);
out.push(p);
}
return out;
}
function main() {
const toc = parseToc();
const pages = loadAllPages(BOOK_DIR);
const patterns = [];
for (const entry of toc) {
const page = resolvePageForEntry(entry, pages);
// 圖鑑只收錄能回到實際 MD 教材的項目,避免目錄文字變成空卡。
if (!page) continue;
patterns.push(...applyOverride(entry, page));
}
for (const extra of overrides.extraPatterns || []) {
const page = pages.find(p => p.pageNum === extra.bookPage);
patterns.push(buildPattern(
{ name: extra.name, category: extra.category, chapter: extra.chapter, chapterTitle: CHAPTER_NAMES[extra.chapter], fullTitle: extra.name },
page,
extra,
));
}
// 補充前言與導讀頁004 等)若未出現在 TOC
const coveredPages = new Set(patterns.map(p => p.bookPage).filter(Boolean));
for (const page of pages) {
if (coveredPages.has(page.pageNum) || page.pageNum <= 4) continue;
if (page.summary.length < 20) continue;
const entry = {
chapter: page.pageNum <= 13 ? 1 : page.pageNum <= 19 ? 2 : page.pageNum <= 35 ? 3 : page.pageNum <= 49 ? 4 : page.pageNum <= 63 ? 5 : 6,
chapterTitle: '',
category: '書頁',
name: page.headline || page.titleLines[page.titleLines.length - 1] || `${page.pageNum}`,
fullTitle: page.titleBlob.slice(0, 60),
};
entry.chapterTitle = CHAPTER_NAMES[entry.chapter] || '';
patterns.push(buildPattern(entry, page, {}));
}
const final = dedupePatterns(patterns).sort((a, b) => {
if (a.chapter !== b.chapter) return a.chapter - b.chapter;
if ((a.bookPage || 999) !== (b.bookPage || 999)) return (a.bookPage || 999) - (b.bookPage || 999);
return a.name.localeCompare(b.name, 'zh-Hant');
});
const catalog = {
version: 2,
source: '短線交易日線圖大全',
builtAt: new Date().toISOString(),
stats: {
total: final.length,
automatable: final.filter(p => p.automatable).length,
chapters: Object.keys(CHAPTER_NAMES).map(Number).filter(n => final.some(p => p.chapter === n)),
},
chapterNames: CHAPTER_NAMES,
patterns: final,
};
fs.writeFileSync(OUT_PATH, JSON.stringify(catalog, null, 2) + '\n', 'utf8');
console.log(`✓ catalog.json${final.length} 筆(可掃描 ${catalog.stats.automatable})→ ${OUT_PATH}`);
}
main();