ราคาทองไทย ทองโลก หุ้น SET/US คริปโต และค่าเงินบาทจากแหล่งข้อมูลจริง พร้อม watchlist พอร์ต และแจ้งเตือนราคา

ดูราคาทองคำแท่งและทองรูปพรรณของไทย ทองโลก หุ้น SET และหุ้นอเมริกา คริปโต (ราคาเป็นบาท) และค่าเงิน USD/THB จากแหล่งข้อมูลจริง ทุกราคาบอกแหล่งที่มาและเวลาของข้อมูล มี watchlist ในแผงด้านข้าง พอร์ตที่คำนวณกำไร/ขาดทุนรวมเป็นบาท และแจ้งเตือนเมื่อราคาข้ามระดับที่ตั้งไว้
| คำสั่ง | ทำอะไร | |
|---|---|---|
/market | watchlist + เปิดแผง | |
/market gold | ทองคำแท่ง ทองรูปพรรณ และทองโลก | |
/market oil | น้ำมันดิบ WTI และ Brent (USD ต่อบาร์เรล) | |
/market crypto [BTC ETH ...] | คริปโต ราคาเป็นบาท และการเปลี่ยนแปลง 24 ชม. | |
/market stock PTT.BK AAPL | หุ้น (หุ้นไทยลงท้าย .BK) | |
/market set PTT KBANK | หุ้นไทยแบบไม่ต้องใส่ .BK | |
/market stocks | หุ้นไทยขนาดใหญ่ที่คนดูบ่อย | |
/market fx | ค่าเงิน USD/THB | |
/market BTC PTT.BK | ดูราคาตามชื่อที่พิมพ์ | |
| `/market watch add\ | remove <ชื่อ>` | จัดการ watchlist |
/market buy <ชื่อ> <จำนวน> <ราคาต่อหน่วย> | บันทึกเข้าพอร์ต | |
/market sell <ชื่อ> | เอาออกจากพอร์ต | |
/market portfolio | กำไร/ขาดทุน รวมเป็นบาท | |
| `/market alert <ชื่อ> above\ | below <ราคา>` | แจ้งเตือนเมื่อราคาข้ามระดับ (เช็คทันที แล้วเช็คทุก 5 นาที) |
/market alerts / alert remove <id> | ดู / ลบการแจ้งเตือน |
ชื่อพิเศษ: GOLD-BAR (ทองคำแท่ง), GOLD (ทองรูปพรรณ), XAU (ทองโลก), OIL/WTI และ BRENT (น้ำมัน), SILVER (เงิน), USDTHB, BTC ETH SOL BNB XRP DOGE ADA USDT
/plugin install market-tracker-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
| ข้อมูล | แหล่ง | หมายเหตุ |
|---|---|---|
| คริปโต | CoinGecko API | ฟรี มีจำกัดจำนวนครั้งต่อนาที |
| ค่าเงิน | open.er-api.com | อัปเดตวันละครั้ง |
| ทองไทย | api.chnwt.dev/thai-gold-api | ไม่เป็นทางการ อ้างอิงราคาจากเว็บสมาคมค้าทองคำ |
| หุ้นและทองโลก | Yahoo Finance chart API | ไม่เป็นทางการ อาจล่าช้า ~15 นาที |
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
Quote เดียวกันPromise.all$.store และเช็คแจ้งเตือนด้วย $.clock.everyhooks/register.tsx 408 lines1/**
2 * ============================================================================
3 * MOD 17: Market Tracker TH - ราคาทอง หุ้น คริปโต และค่าเงินจากแหล่งข้อมูลจริง (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: แอปดูราคาตลาดบนมือถือ
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. ราคาทองคำแท่งและทองรูปพรรณของไทย, ทองโลก, หุ้น SET และหุ้น US, คริปโต, ค่าเงิน USD/THB
9 * 2. Watchlist ในแผงด้านข้าง
10 * 3. พอร์ต: บันทึกต้นทุน แล้วดูกำไร/ขาดทุนตามราคาล่าสุด (รวมเป็นบาท)
11 * 4. แจ้งเตือนเมื่อราคาข้ามระดับที่ตั้งไว้ (เช็คทุก 5 นาทีขณะเปิด Claude Code)
12 *
13 * แหล่งข้อมูล (ฟรี ไม่ต้องใช้ key):
14 * - คริปโต: CoinGecko API
15 * - ค่าเงิน: open.er-api.com (อัปเดตวันละครั้ง)
16 * - ทองไทย: api.chnwt.dev/thai-gold-api (ไม่เป็นทางการ อ้างอิงราคาสมาคมค้าทองคำ)
17 * - หุ้นและทองโลก: Yahoo Finance chart API (ไม่เป็นทางการ อาจล่าช้า ~15 นาที)
18 *
19 * ⚠️ ข้อมูลอาจล่าช้าหรือคลาดเคลื่อน ไม่ใช่คำแนะนำการลงทุน
20 */
21
22import type { EngineInterface, Register, Timer } from 'claude-code'
23
24interface Quote {
25 symbol: string
26 name: string
27 price: number
28 currency: string
29 changePct?: number
30 source: string
31 time: number // ms
32}
33
34interface Holding {
35 symbol: string
36 amount: number
37 cost: number // ราคาต่อหน่วยตอนซื้อ (สกุลเดียวกับ quote)
38}
39
40interface PriceAlert {
41 id: number
42 symbol: string
43 condition: 'above' | 'below'
44 target: number
45}
46
47const PANE = 'market-tracker-th'
48const UA = { 'User-Agent': 'Mozilla/5.0 (claude-code market-tracker-th)' }
49
50// ชื่อย่อคริปโต -> id ของ CoinGecko
51const CRYPTO: Record<string, string> = { BTC: 'bitcoin', ETH: 'ethereum', SOL: 'solana', BNB: 'binancecoin', XRP: 'ripple', DOGE: 'dogecoin', ADA: 'cardano', USDT: 'tether' }
52
53// ==========================================================================
54// บทที่ 1: ดึงข้อมูลแต่ละแหล่ง (แต่ละแหล่งตอบ JSON คนละแบบ ต้องแปลงเป็น Quote เดียวกัน)
55// ==========================================================================
56async function getJson($: EngineInterface, url: string): Promise<Record<string, unknown>> {
57 const res = await $.http.fetch(url, { headers: UA })
58 if (!res.ok) throw new Error(`HTTP ${res.status}`)
59 return JSON.parse(res.text) as Record<string, unknown>
60}
61
62async function cryptoQuotes($: EngineInterface, symbols: string[]): Promise<Quote[]> {
63 const ids = symbols.map(s => CRYPTO[s] ?? s.toLowerCase())
64 const body = await getJson($, `https://api.coingecko.com/api/v3/simple/price?ids=${ids.join(',')}&vs_currencies=thb,usd&include_24hr_change=true&include_last_updated_at=true`)
65 const now = await $.clock.now()
66 return symbols.flatMap((symbol, i) => {
67 const row = body[ids[i] ?? ''] as { thb?: number; usd?: number; thb_24h_change?: number; last_updated_at?: number } | undefined
68 if (!row?.thb) return []
69 return [{ symbol, name: ids[i] ?? symbol, price: row.thb, currency: 'THB', changePct: row.thb_24h_change, source: 'CoinGecko', time: (row.last_updated_at ?? 0) * 1000 || now }]
70 })
71}
72
73async function yahooQuote($: EngineInterface, symbol: string): Promise<Quote> {
74 const body = await getJson($, `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?range=1d&interval=1d`)
75 const chart = body.chart as { result?: { meta?: Record<string, unknown> }[] | null; error?: { description?: string } | null }
76 const meta = chart.result?.[0]?.meta
77 if (!meta || typeof meta.regularMarketPrice !== 'number') throw new Error(chart.error?.description ?? `ไม่พบ ${symbol}`)
78 const price = meta.regularMarketPrice
79 const prev = Number(meta.chartPreviousClose ?? meta.previousClose ?? 0)
80 return {
81 symbol,
82 name: String(meta.shortName ?? meta.longName ?? symbol),
83 price,
84 currency: String(meta.currency ?? 'USD'),
85 changePct: prev ? ((price - prev) / prev) * 100 : undefined,
86 source: 'Yahoo Finance',
87 time: Number(meta.regularMarketTime ?? 0) * 1000,
88 }
89}
90
91async function thaiGold($: EngineInterface): Promise<Quote[]> {
92 const body = await getJson($, 'https://api.chnwt.dev/thai-gold-api/latest')
93 const r = body.response as { update_date?: string; update_time?: string; price?: Record<string, { buy?: string; sell?: string }> } | undefined
94 const num = (s: string | undefined) => Number((s ?? '').replace(/,/g, ''))
95 const bar = r?.price?.gold_bar
96 const ornament = r?.price?.gold
97 if (!bar?.sell) throw new Error('รูปแบบข้อมูลทองเปลี่ยน')
98 const label = `สมาคมค้าทองคำ ${r?.update_date ?? ''} ${r?.update_time ?? ''}`.trim()
99 const now = await $.clock.now()
100 return [
101 { symbol: 'GOLD-BAR', name: `ทองคำแท่ง 96.5% (รับซื้อ ฿${bar.buy})`, price: num(bar.sell), currency: 'THB', source: label, time: now },
102 { symbol: 'GOLD', name: `ทองรูปพรรณ 96.5% (รับซื้อ ฿${ornament?.buy ?? '-'})`, price: num(ornament?.sell), currency: 'THB', source: label, time: now },
103 ]
104}
105
106async function usdThb($: EngineInterface): Promise<Quote> {
107 const body = await getJson($, 'https://open.er-api.com/v6/latest/USD')
108 const rate = (body.rates as Record<string, number> | undefined)?.THB
109 if (!rate) throw new Error('ไม่มีอัตรา THB')
110 return { symbol: 'USDTHB', name: 'ดอลลาร์ / บาท', price: rate, currency: 'THB', source: 'open.er-api.com (รายวัน)', time: Number(body.time_last_update_unix ?? 0) * 1000 }
111}
112
113// ==========================================================================
114// บทที่ 2: ตัวเลือกแหล่งข้อมูลจากชื่อ + cache 60 วินาที (ไม่ยิง API ถี่เกิน)
115// ==========================================================================
116const cache = new Map<string, { at: number; quote: Quote }>()
117
118// ชื่อเรียกง่ายๆ -> สัญลักษณ์ของ Yahoo (ราคาสัญญาซื้อขายล่วงหน้า เป็น USD)
119const ALIASES: Record<string, string> = { XAU: 'GC=F', 'GOLD-WORLD': 'GC=F', OIL: 'CL=F', WTI: 'CL=F', BRENT: 'BZ=F', SILVER: 'SI=F', XAG: 'SI=F' }
120
121// AAPL คงเดิม, BTC -> CoinGecko, GOLD -> ทองไทย, XAU -> ทองโลก, OIL -> น้ำมัน WTI
122function normalize(input: string): string {
123 const s = input.toUpperCase()
124 return ALIASES[s] ?? s
125}
126
127async function quotes($: EngineInterface, inputs: string[]): Promise<{ quotes: Quote[]; errors: string[] }> {
128 const now = await $.clock.now()
129 const wanted = inputs.map(normalize)
130 const result: Quote[] = []
131 const errors: string[] = []
132 const missing = wanted.filter(s => {
133 const hit = cache.get(s)
134 if (hit && now - hit.at < 60_000) return result.push(hit.quote), false
135 return true
136 })
137
138 const cryptoSyms = missing.filter(s => CRYPTO[s])
139 const goldSyms: string[] = missing.filter(s => s === 'GOLD' || s === 'GOLD-BAR')
140 const others = missing.filter(s => !CRYPTO[s] && !goldSyms.includes(s))
141 const save = (q: Quote) => {
142 cache.set(q.symbol, { at: now, quote: q })
143 if (wanted.includes(q.symbol)) result.push(q)
144 }
145
146 await Promise.all([
147 cryptoSyms.length ? cryptoQuotes($, cryptoSyms).then(qs => qs.forEach(save)).catch(err => errors.push(`คริปโต: ${String(err)}`)) : undefined,
148 goldSyms.length ? thaiGold($).then(qs => qs.forEach(save)).catch(err => errors.push(`ทองไทย: ${String(err)}`)) : undefined,
149 ...others.map(s =>
150 (s === 'USDTHB' ? usdThb($) : yahooQuote($, s)).then(save).catch(err => {
151 const message = err instanceof Error ? err.message : String(err)
152 errors.push(`${s}: ${message === 'HTTP 404' ? 'ไม่พบชื่อนี้ (หุ้นไทยต้องลงท้าย .BK หรือใช้ /market set)' : message}`)
153 }),
154 ),
155 ])
156 // เรียงตามลำดับที่ขอ
157 result.sort((a, b) => wanted.indexOf(a.symbol) - wanted.indexOf(b.symbol))
158 return { quotes: result, errors }
159}
160
161// ==========================================================================
162// บทที่ 3: จัดรูปแบบตัวเลข
163// ==========================================================================
164function fmt(n: number, currency: string): string {
165 const digits = n >= 1000 ? 2 : n >= 1 ? 2 : 6
166 const symbol = currency === 'THB' ? '฿' : currency === 'USD' ? '$' : `${currency} `
167 return `${symbol}${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: digits })}`
168}
169
170function change(pct: number | undefined): string {
171 return pct === undefined || Number.isNaN(pct) ? '' : `${pct >= 0 ? '📈 +' : '📉 '}${pct.toFixed(2)}%`
172}
173
174function quoteLine(q: Quote): string {
175 return `${q.symbol.padEnd(9)} ${fmt(q.price, q.currency).padStart(14)} ${change(q.changePct).padEnd(10)} ${q.name}`
176}
177
178function age(ms: number, now: number): string {
179 const min = Math.floor((now - ms) / 60000)
180 return min < 1 ? 'เมื่อสักครู่' : min < 60 ? `${min} นาทีที่แล้ว` : `${Math.floor(min / 60)} ชม.ที่แล้ว`
181}
182
183const DISCLAIMER = 'ℹ️ ข้อมูลอาจล่าช้าหรือคลาดเคลื่อน ไม่ใช่คำแนะนำการลงทุน'
184
185// ==========================================================================
186// บทที่ 4: watchlist, พอร์ต และแจ้งเตือน (เก็บใน $.store)
187// ==========================================================================
188let watchlist: string[] = ['GOLD-BAR', 'BTC', 'USDTHB', 'PTT.BK', 'AAPL']
189let portfolio: Holding[] = []
190let alerts: PriceAlert[] = []
191let nextAlertId = 0
192let alertTimer: Timer | undefined
193let paneQuotes: Quote[] = []
194
195async function persist($: EngineInterface): Promise<void> {
196 await $.store.set('market', { watchlist, portfolio, alerts, nextAlertId })
197}
198
199async function checkAlerts($: EngineInterface): Promise<void> {
200 if (alerts.length === 0) return
201 const { quotes: qs } = await quotes($, [...new Set(alerts.map(a => a.symbol))])
202 const fired = alerts.filter(a => {
203 const q = qs.find(x => x.symbol === a.symbol)
204 return q && (a.condition === 'above' ? q.price >= a.target : q.price <= a.target)
205 })
206 if (fired.length === 0) return
207 alerts = alerts.filter(a => !fired.includes(a)) // แจ้งครั้งเดียวแล้วลบ (ลบก่อน กันแจ้งซ้ำถ้าขั้นต่อไปพัง)
208 await persist($)
209 for (const a of fired) {
210 const q = qs.find(x => x.symbol === a.symbol)
211 const text = `🔔 ${a.symbol} ${a.condition === 'above' ? 'ขึ้นถึง' : 'ลงถึง'} ${fmt(a.target, q?.currency ?? 'THB')} แล้ว (ตอนนี้ ${fmt(q?.price ?? 0, q?.currency ?? 'THB')})`
212 $.ui.toast(text)
213 try {
214 void $.ui.notify(text, { title: 'Market Tracker' }).catch(() => undefined)
215 } catch {
216 // บางหน้าจอแจ้งเตือนของระบบไม่ได้ ไม่เป็นไร มี toast แล้ว
217 }
218 }
219 startAlertLoop($)
220}
221
222function startAlertLoop($: EngineInterface): void {
223 alertTimer?.cancel()
224 alertTimer = alerts.length ? $.clock.every(5 * 60 * 1000, () => void checkAlerts($).catch(() => undefined)) : undefined
225}
226
227async function refreshPane($: EngineInterface): Promise<string[]> {
228 const { quotes: qs, errors } = await quotes($, watchlist)
229 paneQuotes = qs
230 $.ui.invalidate('ui.render')
231 return errors
232}
233
234const HELP = `📊 Market Tracker TH (Mod 17)
235========================================
236/market watchlist + เปิดแผง
237/market gold ทองคำแท่ง ทองรูปพรรณ ทองโลก
238/market oil น้ำมัน WTI และ Brent (USD/บาร์เรล)
239/market crypto [BTC ETH] คริปโต (ราคาเป็นบาท)
240/market stock <ชื่อ...> หุ้น เช่น PTT.BK CPALL.BK AAPL NVDA
241/market set <ชื่อ...> หุ้นไทย ไม่ต้องใส่ .BK เช่น /market set PTT KBANK
242/market stocks หุ้นไทยขนาดใหญ่ที่คนดูบ่อย
243/market fx ค่าเงิน USD/THB
244/market watch add|remove <ชื่อ>
245/market buy <ชื่อ> <จำนวน> <ราคาต่อหน่วย> บันทึกเข้าพอร์ต
246/market sell <ชื่อ> เอาออกจากพอร์ต
247/market portfolio กำไร/ขาดทุน (รวมเป็นบาท)
248/market alert <ชื่อ> above|below <ราคา> แจ้งเตือนเมื่อราคาข้าม
249/market alerts | alert remove <id>
250ชื่อพิเศษ: GOLD-BAR, GOLD (ทองไทย), XAU (ทองโลก), OIL/WTI, BRENT, SILVER, USDTHB, BTC ETH SOL BNB XRP DOGE ADA USDT`
251
252export const register: Register = on => {
253 on('session.start', async ($, e, next) => {
254 const saved = (await $.store.get('market')) as { watchlist?: string[]; portfolio?: Holding[]; alerts?: PriceAlert[]; nextAlertId?: number } | undefined
255 if (saved?.watchlist) watchlist = saved.watchlist
256 portfolio = saved?.portfolio ?? []
257 alerts = saved?.alerts ?? []
258 nextAlertId = saved?.nextAlertId ?? 0
259 startAlertLoop($)
260 await $.command.register({ name: 'market', description: 'ราคาทอง หุ้น คริปโต ค่าเงิน (/market help)', argumentHint: 'gold|crypto|stock|fx|portfolio|alert' })
261 return next(e)
262 })
263
264 // ==========================================================================
265 // บทที่ 5: แผง watchlist
266 // ==========================================================================
267 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
268 const { Box, Text, Button } = $.ui.resolve(e)
269 const now = await $.clock.now()
270 return (
271 <Box flexDirection="column">
272 {paneQuotes.length === 0 && <Text dimColor>พิมพ์ /market เพื่อโหลดราคา</Text>}
273 {paneQuotes.map(q => (
274 <Box justifyContent="space-between" gap={1}>
275 <Text bold>{q.symbol}</Text>
276 <Text>{fmt(q.price, q.currency)}</Text>
277 <Text color={(q.changePct ?? 0) >= 0 ? '#2e7d32' : '#c62828'}>{change(q.changePct)}</Text>
278 </Box>
279 ))}
280 <Text dimColor>{paneQuotes[0] ? `อัปเดต ${age(Math.min(...paneQuotes.map(q => q.time)), now)}` : ''}</Text>
281 <Button key="refresh" label="รีเฟรช" hotkey="r" onPress={() => void refreshPane($)} />
282 </Box>
283 )
284 })
285
286 on('command.run', { command: 'market' }, async ($, e) => {
287 const [sub = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
288 const now = await $.clock.now()
289
290 // แสดงชุดราคา พร้อมแหล่งที่มาและ error ถ้ามี
291 const show = async (title: string, symbols: string[]) => {
292 const { quotes: qs, errors } = await quotes($, symbols)
293 const sources = [...new Set(qs.map(q => `${q.source} (${age(q.time, now)})`))]
294 return {
295 text: [title, '----------------------------------------', ...qs.map(quoteLine), ...errors.map(err => `⚠️ ${err}`), '', `แหล่งข้อมูล: ${sources.join(', ') || '-'}`, DISCLAIMER].join('\n'),
296 }
297 }
298
299 switch (sub) {
300 case 'help':
301 return { text: HELP }
302 case '': {
303 await $.ui.open({ id: PANE, title: '📊 Market' })
304 await refreshPane($)
305 return show('📊 Watchlist', watchlist)
306 }
307 case 'gold':
308 return show('🥇 ราคาทอง', ['GOLD-BAR', 'GOLD', 'XAU'])
309 case 'oil':
310 return show('🛢️ น้ำมันดิบ (USD/บาร์เรล)', ['OIL', 'BRENT'])
311 case 'crypto':
312 return show('₿ คริปโต (บาท, เปลี่ยนแปลง 24 ชม.)', rest.length ? rest : ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE'])
313 case 'stock':
314 return rest.length ? show('📈 หุ้น', rest) : { text: 'วิธีใช้: /market stock PTT.BK AAPL' }
315 case 'set':
316 return rest.length ? show('📈 หุ้นไทย (SET)', rest.map(s => (s.includes('.') ? s : `${s}.BK`))) : { text: 'วิธีใช้: /market set PTT KBANK' }
317 case 'stocks':
318 // หุ้นไทยขนาดใหญ่ที่คนดูบ่อย (ไม่ใช่คำแนะนำให้ซื้อ)
319 return show('📈 หุ้นไทยขนาดใหญ่ (SET)', ['PTT.BK', 'AOT.BK', 'CPALL.BK', 'ADVANC.BK', 'KBANK.BK', 'SCB.BK', 'BDMS.BK', 'GULF.BK'])
320 case 'fx':
321 return show('💱 ค่าเงิน', ['USDTHB'])
322 case 'watch': {
323 const [action, symbol] = rest
324 if (!symbol || (action !== 'add' && action !== 'remove')) return { text: `👀 Watchlist: ${watchlist.join(', ')}\nวิธีใช้: /market watch add|remove <ชื่อ>` }
325 const s = normalize(symbol)
326 watchlist = action === 'add' ? [...new Set([...watchlist, s])] : watchlist.filter(w => w !== s)
327 await persist($)
328 await refreshPane($)
329 return { text: `👀 Watchlist: ${watchlist.join(', ')}` }
330 }
331 case 'buy': {
332 const [symbol, amountText, costText] = rest
333 const amount = Number(amountText)
334 const cost = Number(costText)
335 if (!symbol || !(amount > 0) || !(cost > 0)) return { text: 'วิธีใช้: /market buy BTC 0.01 2500000 (ชื่อ จำนวน ราคาต่อหน่วยตอนซื้อ)' }
336 portfolio.push({ symbol: normalize(symbol), amount, cost })
337 await persist($)
338 return { text: `✅ บันทึก ${normalize(symbol)} ${amount} หน่วย ต้นทุน ${cost.toLocaleString('en-US')} ต่อหน่วย` }
339 }
340 case 'sell': {
341 const s = normalize(rest[0] ?? '')
342 const before = portfolio.length
343 portfolio = portfolio.filter(h => h.symbol !== s)
344 await persist($)
345 return { text: before === portfolio.length ? `❌ ไม่มี ${s} ในพอร์ต` : `🗑️ เอา ${s} ออกจากพอร์ตแล้ว` }
346 }
347 case 'portfolio': {
348 if (portfolio.length === 0) return { text: '📊 พอร์ตว่าง\nบันทึกด้วย /market buy BTC 0.01 2500000' }
349 const { quotes: qs, errors } = await quotes($, [...new Set([...portfolio.map(h => h.symbol), 'USDTHB'])])
350 const usd = qs.find(q => q.symbol === 'USDTHB')?.price ?? 0
351 const toThb = (n: number, currency: string) => (currency === 'THB' ? n : currency === 'USD' && usd ? n * usd : NaN)
352 let costThb = 0
353 let valueThb = 0
354 const rows = portfolio.map(h => {
355 const q = qs.find(x => x.symbol === h.symbol)
356 if (!q) return `⚠️ ${h.symbol}: ดึงราคาไม่ได้`
357 const pnl = (q.price - h.cost) * h.amount
358 costThb += toThb(h.cost * h.amount, q.currency)
359 valueThb += toThb(q.price * h.amount, q.currency)
360 return `${h.symbol} ${h.amount} × ${fmt(h.cost, q.currency)} → ${fmt(q.price, q.currency)} ${pnl >= 0 ? '📈 +' : '📉 '}${fmt(pnl, q.currency)} (${(((q.price - h.cost) / h.cost) * 100).toFixed(2)}%)`
361 })
362 const total = valueThb - costThb
363 return {
364 text: [
365 '📊 พอร์ต',
366 '----------------------------------------',
367 ...rows,
368 '',
369 Number.isNaN(total) ? 'รวมเป็นบาทไม่ได้ (มีสกุลเงินที่ไม่รู้อัตราแลกเปลี่ยน)' : `💰 ต้นทุน ${fmt(costThb, 'THB')} | มูลค่า ${fmt(valueThb, 'THB')} | ${total >= 0 ? '📈 +' : '📉 '}${fmt(total, 'THB')} (${((total / costThb) * 100).toFixed(2)}%)`,
370 ...errors.map(err => `⚠️ ${err}`),
371 DISCLAIMER,
372 ].join('\n'),
373 }
374 }
375 case 'alert': {
376 if (rest[0] === 'remove') {
377 alerts = alerts.filter(a => a.id !== Number(rest[1]))
378 await persist($)
379 startAlertLoop($)
380 return { text: `🗑️ ลบการแจ้งเตือน #${rest[1] ?? ''} แล้ว` }
381 }
382 const [symbol, condition, targetText] = rest
383 const target = Number((targetText ?? '').replace(/,/g, ''))
384 if (!symbol || (condition !== 'above' && condition !== 'below') || !(target > 0)) return { text: 'วิธีใช้: /market alert BTC above 3000000 หรือ /market alert GOLD-BAR below 60000' }
385 alerts.push({ id: nextAlertId++, symbol: normalize(symbol), condition, target })
386 await persist($)
387 startAlertLoop($)
388 await checkAlerts($).catch(() => undefined) // เช็คครั้งแรกทันที ไม่ต้องรอ 5 นาที
389 return { text: `🔔 ตั้งแจ้งเตือน ${normalize(symbol)} ${condition === 'above' ? 'ขึ้นถึง' : 'ลงถึง'} ${target.toLocaleString('en-US')} (เช็คทุก 5 นาทีขณะเปิด Claude Code)` }
390 }
391 case 'alerts':
392 return { text: alerts.length ? ['🔔 การแจ้งเตือน', ...alerts.map(a => ` #${a.id} ${a.symbol} ${a.condition === 'above' ? '≥' : '≤'} ${a.target.toLocaleString('en-US')}`)].join('\n') : 'ยังไม่มีการแจ้งเตือน' }
393 default:
394 // /market BTC PTT.BK = ดูราคาตามชื่อที่พิมพ์
395 return show('📊 ราคา', [sub, ...rest])
396 }
397 })
398}
399
400/**
401 * ========================================================================
402 * การบ้าน / ต่อยอด
403 * ========================================================================
404 * 1. วาดกราฟราคาย้อนหลัง: Yahoo chart API ใส่ range=1mo&interval=1d ได้ราคาปิดรายวัน
405 * 2. ส่งแจ้งเตือนราคาเข้า LINE ด้วย Flex Message ของ Mod 12
406 * 3. คำนวณ DCA: บันทึกการซื้อหลายครั้ง แล้วหาต้นทุนเฉลี่ย
407 */
408