// ═══════════════════════════════════════════════════════════ // 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();