去年我一直有個困擾:每天早上開盤前,要自己去查台指期夜盤收盤、看外資期貨未平倉空單、找漲停族群、確認有沒有處置股……光是蒐集資料就要花 20–30 分鐘,而且常常遺漏某些資訊。
後來我花了一個晚上,用 Claude 幫我建了一套系統,現在每天早上 7:30,把每天要盯的數據、股票標的,製作成「獵犬交易日報」自動寄到我的電子信箱。
日報包含以下內容:
▸ CNN 恐懼貪婪指數(情緒判斷)
▸ S&P 500、那斯達克、VIX、油價、黃金、美元(市場體溫)
▸ 外資台指期未平倉(籌碼方向)
▸ 外資現貨買超/賣超前五名(法人動向)
▸ 昨日漲停個股與族群分類(資金流向)
▸ 類股漲跌幅排行(強弱輪動)
▸ 處置股進出關名單(風險雷達)
▸ AI 整理的台股產業新聞與法說會重點
這篇文章把整個過程完整記錄下來,從零開始,不需要懂程式,跟著做就可以。
你需要準備什麼
必要:
▸ Gmail 帳號(用來發送日報;免費)
▸ Google 帳號(登入 Google Apps Script;免費)
▸ Claude Pro方案(每月20美元)
▸ Claude API Key(需要儲值,每天費用約 NT$0.5–1 元)
選填(免費,但強烈建議):
▸ RapidAPI Key(用來取得真正的 CNN 恐懼貪婪指數,API可免費連結多種金融指數)
這套系統不需要任何主機、不需要 GitHub、不需要安裝任何軟體。全部跑在 Google 的雲端上。
整體架構:三個部件,自動運作
在開始設定之前,先理解整個系統的邏輯:
Google Apps Script(免費雲端排程)
↓ 每天早上 07:30 自動執行
Yahoo Finance → 美股指數
臺灣期交所 → 外資未平倉
臺灣證交所 → 外資買超/成交量
Alternative.me → 恐懼貪婪指數
Claude API → AI 新聞摘要
↓
Gmail 寄送 → 你的信箱
最重要的概念:資料從官方來源直接抓取,不是 AI 瞎掰的。外資未平倉來自臺灣期交所官方 CSV,外資買超來自臺灣證交所開放資料,美股數據來自 Yahoo Finance API。AI 只負責整理「文字類」的新聞摘要、漲停族群分類、處置股資訊。
STEP 1 取得 Claude API Key(約 10 分鐘)
Claude API Key 是整個系統的核心,讓程式可以呼叫 Claude 來整理新聞。
操作步驟:
▸ 開啟瀏覽器,前往 console.anthropic.com
▸ 點右上角「Sign Up」用 Google 帳號註冊(或直接登入)
▸ 登入後,左側選單點「API Keys」
▸ 點「Create Key」,Name 輸入「獵犬交易日報」,點 Create(名稱取自己看得懂就好)
▸ 畫面顯示 sk-ant-api03- 開頭的金鑰—立刻複製,這是唯一一次顯示的機會
▸ 把這串 Key 暫時貼到記事本備用
儲值方式:
左側選單點「Billing」→「Buy credits」→ 最低儲值 $5 美金(約 NT$160)。每天的日報約消耗 $0.01–0.02,$5 可以跑 6–12 個月。
注意:Anthropic 有時會發放免費額度給新帳號,但這些免費額度不能用於 API 呼叫,一定要自己購買 credits 才有效。
STEP 2 取得 RapidAPI Key(約 5 分鐘,選填)
這個步驟讓你的恐懼貪婪指數從「加密市場版」升級成「CNN 官方版」。不做的話日報還是可以運作,只是這個欄位的數據來源會不同。
▸ 前往 rapidapi.com → 用 Google 帳號登入
▸ 上方搜尋欄輸入「Fear and Greed Index」
▸ 找到「Fear and greed index」(By Tony,第三橫排最左邊,評分 9.4,速度 634ms,穩定度 100%;重點這個免費)→ 點進去
▸ 右上角點「Subscribe to Test」→ 選「Basic(免費)」→ Subscribe
▸ 訂閱後,左側頁面顯示你的「X-RapidAPI-Key」→ 複製備用
STEP 3 開啟 Google Apps Script 並貼上程式碼(約 5 分鐘)
Google Apps Script 是 Google 提供的免費雲端程式執行環境,可以定時自動執行腳本、發送 Gmail。你不需要懂程式,只要知道「把程式碼貼進去」就好。
▸ 開啟瀏覽器,前往 script.google.com(用 Gmail 帳號登入)
▸ 點左上角「新專案」
▸ 畫面出現編輯視窗,裡面有預設程式碼
▸ 把預設程式碼全部選取後刪除
▸ 將本文附件的完整程式碼複製,貼入空白的編輯視窗
▸ 按 Ctrl+S 儲存,輸入專案名稱「獵犬交易日報」(取自己看得懂的名稱)→ 確定
STEP 4 填入你的 API Key(約 2 分鐘)
程式碼最開頭有一個 CONFIG 設定區,這是你唯一需要修改的地方:
const CONFIG = {
RECIPIENT_EMAIL: "你的信箱@gmail.com",
SENDER_NAME: "日報名稱",
ANTHROPIC_API_KEY: "sk-ant-api03-你的Key",
RAPIDAPI_KEY: "你的RapidAPIKey",
};
填好後按 Ctrl+S 存檔。
安全提示:這份程式碼存在你自己的 Google 帳號裡,API Key 不會外流。但請不要把含有 Key 的程式碼截圖分享給別人,也千萬不要在過程中回傳給Claude。
STEP 5 第一次執行測試(約 3 分鐘)
▸ 在編輯視窗上方的函式選單,確認選的是「sendDailyReport」
▸ 點左側的「▶ 執行」按鈕
▸ 第一次執行會彈出「需要授權」視窗
授權步驟(只需要做一次):
▸ 點「審查權限」→ 選擇你的 Gmail 帳號
▸ 出現「這個應用程式未經 Google 驗證」的警告頁面—這是正常的,因為這是你自己寫的程式
▸ 點左下角「進階」→「前往獵犬交易日報(不安全)」→「允許」
授權完成後,程式會開始執行(約 2–3 分鐘)。執行完成後,去收信匣確認有沒有收到測試信。
如果沒收到,看執行記錄找原因:
▸ 401 invalid x-api-key → Claude API Key 填錯,重新複製貼上
▸ Your credit balance is too low → Anthropic 帳號餘額不足,去 Billing 儲值
▸ 找不到 JSON 區塊 → 正常偶發現象,重新執行即可
p.s.萬一都不是以上訊息,沒關係,把原因貼給Claude,請他告訴你下一步該怎麼做。
STEP 6 設定每天自動執行(約 3 分鐘)
測試信沒問題後,設定排程讓程式每天自動跑:
▸ 左側選單點「時鐘圖示」(觸發條件)
▸ 右下角點「+ 新增觸發條件」
▸ 選擇您要執行的功能:sendDailyReport
▸ 選取活動來源:時間驅動
▸ 選取時間型觸發條件類型:日計時器
▸ 選取小時間隔:上午 7:00 至上午 8:00
▸ 點「儲存」
完成。從明天開始,每天早上 7:00–8:00 之間,日報會自動寄到你的信箱。
日報實際畫面:
日報內容說明:每個區塊的意義
§01 市場體溫
CNN 恐懼貪婪指數是市場情緒的「體溫計」。0–24 是極度恐懼,這個區間歷史上往往是逢低布局的時機;75–100 是極度貪婪,市場通常過熱需要小心。搭配 VIX 一起看:VIX 突然飆升代表市場恐慌情緒急劇升溫。
§02 台股籌碼雷達
外資台指期淨部位是最直接的籌碼訊號。淨空單持續減少(回補),是多頭方向的正面訊號;若淨空單急增,要提高警覺。外資現貨買超/賣超則告訴你,法人的實際資金在買哪些股、賣哪些股,比散戶追漲停更有參考價值。
§03 資金流向偵測
漲停個股依族群分類,讓你一眼看出「今天的資金在追什麼題材」。如果同一個族群連續兩天出現在漲停名單,那個族群就值得深入研究。類股漲跌幅排行則是判斷「輪動方向」的工具。
§04 處置股雷達
處置股對短線交易者非常重要。進入 5 分盤或 20 分盤處置後,成交效率大幅降低,短線流動性變差,容易造成流動性風險。反過來說,即將出關的股票,在出關前後往往有價格修復的機會。
§05–06 總經雷達與產業法說
這兩個區塊由 Claude 透過即時搜尋整理,幫你快速掌握昨晚美股動態、台股產業新聞,以及當天或本週的重要法說會重點。
常見問題
Q:這樣的系統安全嗎?API Key 會不會外洩?
程式碼存在你自己的 Google 帳號,和你的 Gmail 在同一個環境裡。API Key 只存在你的 Apps Script 程式碼裡,不會傳送到任何第三方伺服器。唯一要注意的是:不要把含有 Key 的程式碼截圖或複製給別人。
Q:每天費用大概多少?
▸ Anthropic Claude API:每天約 $0.01–0.02 美金(NT$0.3–0.6 元)
▸ Yahoo Finance、臺灣期交所、臺灣證交所數據:完全免費
▸ RapidAPI Fear & Greed:免費方案每月 500 次,每天一次用不完
Q:週末也會寄嗎?
會。週末日報的市場數據會自動取最近一個交易日的數據,並標示日期讓你知道資料的時間點。
Q:可以加入自己持有的個股監控嗎?
可以。在程式碼的 fetchUsMarkets 函式的 symbols 字典裡加入:
"2330.TW": "台積電",
"NVDA": "NVIDIA",
重新儲存執行,日報的市場指標表格就會多出這兩行。
寫在最後
這套系統我自己在用,它幫我把每天早上的資料蒐集時間從 30 分鐘壓縮到 0 分鐘。
但我要特別說明:日報是輔助工具,不是交易訊號。數據告訴你市場發生了什麼,判斷和決策還是你自己的責任。外資買超不代表一定漲,恐懼指數低也不代表明天就會反彈。這份日報的價值在於「讓你在每天開盤前,用最短的時間掌握最完整的市場輪廓」。
如果你照著這篇教學做出了自己的交易日報,或是過程有遇到任何問題,歡迎留言給我。
本文附件提供完整程式碼,貼入 Google Apps Script 即可使用
// ══════════════════════════════════════════════════════════
// 🐕 獵犬交易日報 v3.1 — Google Apps Script
// ══════════════════════════════════════════════════════════
const CONFIG = {
RECIPIENT_EMAIL: “",// ← 填入收信mail
SENDER_NAME: "",// ← 幫日報取一個名稱
ANTHROPIC_API_KEY: "", // ← 貼上您的 Claude API Key
RAPIDAPI_KEY: "", // ← 選填:CNN Fear & Greed
};
// ══════════════════════════════════════════════════════════
// 主程序
// ══════════════════════════════════════════════════════════
function sendDailyReport() {
const today = new Date();
Logger.log("🐕 獵犬交易日報 v3.1 啟動:" + Utilities.formatDate(today, "Asia/Taipei", "yyyy/MM/dd HH:mm:ss"));
const fg = fetchFearGreed();
const markets = fetchUsMarkets();
const oi = fetchTaifexOI();
const twData = fetchTwseData();
const disposal = fetchDisposal();
const aiData = fetchAllAiData(twData.limitUpStocks, twData.dateDisplay);
const html = buildHtml(today, fg, markets, oi, twData, aiData, disposal);
const wd = ["日","一","二","三","四","五","六"][today.getDay()];
const subject = "【獵犬】交易日報 " + fmtDate(today) + " (週" + wd + ")";
GmailApp.sendEmail(
CONFIG.RECIPIENT_EMAIL,
subject,
"請使用支援 HTML 的郵件客戶端閱讀本報告。",
{ htmlBody: html, name: CONFIG.SENDER_NAME }
);
Logger.log("✅ 寄送完成 → " + CONFIG.RECIPIENT_EMAIL);
}
// ══════════════════════════════════════════════════════════
// §01 市場體溫 — Fear & Greed
// ══════════════════════════════════════════════════════════
function fetchFearGreed() {
if (CONFIG.RAPIDAPI_KEY) {
try {
const res = UrlFetchApp.fetch(
"https://fear-greed-index1.p.rapidapi.com/web-crawling/api/fear-greed-index",
{
headers: {
"X-RapidAPI-Host": "fear-greed-index1.p.rapidapi.com",
"X-RapidAPI-Key": CONFIG.RAPIDAPI_KEY,
},
muteHttpExceptions: true,
}
);
if (res.getResponseCode() === 200) {
const data = JSON.parse(res.getContentText());
const v = Math.round(parseFloat(data.fear_and_greed.score));
return { value: v, label: fgLabel(v), source: "CNN / RapidAPI ✓" };
}
Logger.log("F&G RapidAPI 失敗:" + res.getResponseCode());
} catch(e) { Logger.log("F&G RapidAPI 異常:" + e); }
}
return { value: "N/A", label: "請至 CNN Business 確認", source: "CNN / RapidAPI(今日配額已用完)" };
}
function fgLabel(v) {
if (v < 25) return "極度恐懼 Extreme Fear";
if (v < 45) return "恐懼 Fear";
if (v < 56) return "中性 Neutral";
if (v < 75) return "貪婪 Greed";
return "極度貪婪 Extreme Greed";
}
function fgColor(v) {
if (typeof v !== "number") return "#6b7280";
if (v < 25) return "#dc2626";
if (v < 45) return "#ea580c";
if (v < 56) return "#ca8a04";
if (v < 75) return "#16a34a";
return "#15803d";
}
// ══════════════════════════════════════════════════════════
// §01 市場體溫 — 美股指數
// ══════════════════════════════════════════════════════════
function fetchUsMarkets() {
const symbols = {
"^GSPC": "S&P 500",
"^IXIC": "那斯達克",
"^VIX": "VIX 恐慌指數",
"^TNX": "美10Y公債殖利率(%)",
"BZ=F": "布蘭特原油",
"GC=F": "黃金",
"TWD=X": "USD/TWD",
};
const results = {};
for (const [sym, name] of Object.entries(symbols)) {
try {
const url = "https://query1.finance.yahoo.com/v8/finance/chart/" + encodeURIComponent(sym) + "?interval=1d&range=2d";
const res = UrlFetchApp.fetch(url, {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
if (res.getResponseCode() === 200) {
const meta = JSON.parse(res.getContentText()).chart.result[0].meta;
const price = meta.regularMarketPrice || meta.previousClose || 0;
const prev = meta.chartPreviousClose || meta.previousClose || price;
const chg = price - prev;
const pct = prev ? (chg / prev * 100) : 0;
results[sym] = { name, price: round(price,4), change: round(chg,4), pct: round(pct,2) };
} else {
results[sym] = { name, price: "N/A", change: 0, pct: 0 };
}
} catch(e) {
results[sym] = { name, price: "N/A", change: 0, pct: 0 };
}
Utilities.sleep(200);
}
return results;
}
// ══════════════════════════════════════════════════════════
// §02 台股籌碼 — 外資期貨未平倉
// ══════════════════════════════════════════════════════════
function fetchTaifexOI() {
const today = new Date();
const dow = today.getDay();
const offset = dow === 0 ? 4 : dow === 6 ? 3 : 1;
const qDate = new Date(today.getTime() - offset * 86400000);
const dateStr = Utilities.formatDate(qDate, "Asia/Taipei", "yyyy/MM/dd");
// 前一交易日(再往前一天)
const prevOffset = offset + (qDate.getDay() === 1 ? 3 : 1); // 週一的前一交易日是週五
const prevDate = new Date(today.getTime() - prevOffset * 86400000);
const prevDateStr = Utilities.formatDate(prevDate, "Asia/Taipei", "yyyy/MM/dd");
function fetchOI(ds) {
try {
const url = "https://www.taifex.com.tw/cht/3/futContractsDateDown"
+ "?queryStartDate=" + ds + "&queryEndDate=" + ds + "&commodity_id=TXF";
const res = UrlFetchApp.fetch(url, {
headers: { "User-Agent": "Mozilla/5.0", "Referer": "https://www.taifex.com.tw/" },
muteHttpExceptions: true,
});
if (res.getResponseCode() !== 200) return null;
const bytes = res.getContent();
const text = Utilities.newBlob(bytes).setContentType("text/plain; charset=big5").getDataAsString();
const rows = text.split("\n").map(function(r) {
return r.split(",").map(function(c) { return c.replace(/"/g,"").trim(); });
});
if (rows[3] && rows[3].length >= 14) {
const safeInt = function(s) { return parseInt(String(s).replace(/,/g,"")) || 0; };
return {
longOI: safeInt(rows[3][9]),
shortOI: safeInt(rows[3][11]),
net: safeInt(rows[3][13]),
};
}
} catch(e) { Logger.log("期交所抓取失敗(" + ds + "):" + e); }
return null;
}
const cur = fetchOI(dateStr);
const prev = fetchOI(prevDateStr);
if (cur) {
const prevNet = prev ? prev.net : null;
const diff = (prev && cur) ? cur.net - prev.net : null;
Logger.log("期交所 OI:今 " + cur.net + " 前 " + (prevNet || "N/A") + " 差 " + (diff || "N/A"));
return {
longOI: cur.longOI,
shortOI: cur.shortOI,
net: cur.net,
prevNet: prevNet,
diff: diff,
date: dateStr,
prevDate: prevDateStr,
ok: true,
};
}
return { longOI: "N/A", shortOI: "N/A", net: "N/A", prevNet: null, diff: null, date: dateStr, ok: false };
}
// ══════════════════════════════════════════════════════════
// §02 台股籌碼 — 外資現貨買超 + 成交量 + 官方漲停名單
// ══════════════════════════════════════════════════════════
function fetchTwseData() {
const today = new Date();
const dow = today.getDay();
const offset = dow === 0 ? 3 : dow === 6 ? 2 : 1;
const qDate = new Date(today.getTime() - offset * 86400000);
const dateStr = Utilities.formatDate(qDate, "Asia/Taipei", "yyyyMMdd");
const dateDisplay = Utilities.formatDate(qDate, "Asia/Taipei", "M/d");
var foreignBuy = [];
var topVolume = [];
var limitUpStocks = [];
// 外資買賣超(T86)
try {
const url = "https://www.twse.com.tw/rwd/zh/fund/T86?date=" + dateStr + "&selectType=ALL&response=json";
const res = UrlFetchApp.fetch(url, { headers: { "User-Agent": "Mozilla/5.0" }, muteHttpExceptions: true });
if (res.getResponseCode() === 200) {
const json = JSON.parse(res.getContentText());
if (json.data && json.data.length > 0) {
var rows = json.data.slice();
rows.sort(function(a, b) {
return (parseInt(String(b[4]).replace(/,/g,"")) || 0) - (parseInt(String(a[4]).replace(/,/g,"")) || 0);
});
rows.slice(0, 5).forEach(function(r) {
foreignBuy.push({ code: r[0], name: r[1], net: parseInt(String(r[4]).replace(/,/g,"")) || 0, type: "買超" });
});
rows.slice(-5).reverse().forEach(function(r) {
foreignBuy.push({ code: r[0], name: r[1], net: parseInt(String(r[4]).replace(/,/g,"")) || 0, type: "賣超" });
});
}
}
} catch(e) { Logger.log("外資買賣超失敗:" + e); }
// 成交量排行(MI_INDEX20)
try {
const url2 = "https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX20?date=" + dateStr + "&response=json";
const res2 = UrlFetchApp.fetch(url2, { headers: { "User-Agent": "Mozilla/5.0" }, muteHttpExceptions: true });
if (res2.getResponseCode() === 200) {
const json2 = JSON.parse(res2.getContentText());
if (json2.data && json2.data.length > 0) {
json2.data.slice(0, 5).forEach(function(r) {
topVolume.push({ code: r[0], name: r[1], volume: r[2], close: r[8], change: r[9] });
});
}
}
} catch(e) { Logger.log("成交量排行失敗:" + e); }
// 官方漲停名單:MI_INDEX table[7] 只有統計,改用 STOCK_DAY_ALL CSV 計算
// 方案A:直接捨棄 MI_INDEX 漲停查詢
// 方案B:STOCK_DAY_ALL CSV 解析(正確欄位版)
try {
const url4 = "https://www.twse.com.tw/rwd/zh/afterTrading/STOCK_DAY_ALL?date=" + dateStr;
const res4 = UrlFetchApp.fetch(url4, {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
if (res4.getResponseCode() === 200) {
const lines = res4.getContentText().split("\n");
lines.forEach(function(line, idx) {
if (idx === 0) return; // 跳過 header
// 去除引號,分割欄位
var cols = line.split('","');
if (cols.length < 10) return;
cols[0] = cols[0].replace(/^"/, ""); // 去掉第一個欄位開頭的引號
cols[cols.length-1] = cols[cols.length-1].replace(/"$/, ""); // 去掉最後欄位結尾引號
// 欄位:[0]日期 [1]代號 [2]名稱 [3]成交股數 [4]成交金額
// [5]開盤 [6]最高 [7]最低 [8]收盤 [9]漲跌價差 [10]成交筆數
const code = cols[1] ? cols[1].trim() : "";
const name = cols[2] ? cols[2].trim() : "";
const close = parseFloat(cols[8]) || 0;
const change = parseFloat(cols[9]) || 0;
const prevClose = close - change;
const pct = prevClose > 0 ? (change / prevClose * 100) : 0;
if (pct >= 9.5 && code && name) {
limitUpStocks.push({ code: code, name: name, pct: Math.round(pct * 10) / 10 });
}
});
Logger.log("漲停(STOCK_DAY_ALL):" + limitUpStocks.length + " 檔");
}
} catch(e) { Logger.log("STOCK_DAY_ALL 失敗:" + e); }
return {
foreignBuy: foreignBuy,
topVolume: topVolume,
limitUpStocks: limitUpStocks,
date: dateStr,
dateDisplay: dateDisplay,
};
}
function fetchDisposal() {
var listed = []; // 上市處置
var otc = []; // 上櫃處置
var today = Utilities.formatDate(new Date(), "Asia/Taipei", "yyyyMMdd");
// ── 上市:TWSE JSON API ──
try {
const r = UrlFetchApp.fetch(
"https://www.twse.com.tw/rwd/zh/announcement/punish?response=json",
{ headers: { "User-Agent": "Mozilla/5.0" }, muteHttpExceptions: true }
);
if (r.getResponseCode() === 200) {
const json = JSON.parse(r.getContentText());
if (json.data && json.data.length > 0) {
json.data.forEach(function(row) {
// row: [編號, 公布日期, 代號, 名稱, 累計, 條件, 起迄時間, 措施, 內容, 備註]
var code = row[2] || "";
var name = row[3] || "";
var period = row[6] || ""; // 例:115/07/03~115/07/16
var content = row[8] || "";
var measure = row[7] || ""; // 第一次/第二次處置
// 判斷 5 分盤或 20 分盤
var boardType = "5分盤";
if (content.indexOf("二十") >= 0 || content.indexOf("20分") >= 0 || measure.indexOf("第二次") >= 0) {
boardType = "20分盤";
}
// 轉換民國年日期為西元
// 期交所格式:115/07/03~115/07/16
var periodWest = period.replace(/^(\d+)\/(\d+)\/(\d+)~(\d+)\/(\d+)\/(\d+)$/, function(m, y1, m1, d1, y2, m2, d2) {
return (parseInt(y1)+1911) + "/" + m1 + "/" + d1 + "~" + (parseInt(y2)+1911) + "/" + m2 + "/" + d2;
});
// 判斷是否還在處置期間(結束日期 >= 今天)
var endPart = period.split("~")[1] || "";
var endMatch = endPart.match(/(\d+)\/(\d+)\/(\d+)/);
var isActive = true;
if (endMatch) {
var endYear = parseInt(endMatch[1]) + 1911;
var endMonth = parseInt(endMatch[2]);
var endDay = parseInt(endMatch[3]);
var endDate = endYear * 10000 + endMonth * 100 + endDay;
isActive = endDate >= parseInt(today);
}
if (isActive && code && name) {
listed.push({
code: code, name: name,
boardType: boardType,
period: periodWest,
});
}
});
}
Logger.log("上市處置:" + listed.length + " 檔");
}
} catch(e) { Logger.log("上市處置失敗:" + e); }
// ── 上櫃:TPEX OpenAPI ──
try {
const r2 = UrlFetchApp.fetch(
"https://www.tpex.org.tw/openapi/v1/tpex_disposal_information",
{ headers: { "User-Agent": "Mozilla/5.0" }, muteHttpExceptions: true }
);
if (r2.getResponseCode() === 200) {
const arr = JSON.parse(r2.getContentText());
arr.forEach(function(item) {
var code = item.SecuritiesCompanyCode || "";
var name = item.CompanyName || "";
var period = item.DispositionPeriod || ""; // 例:1150710~1150723
var reason = item.DisposalCondition || "";
// 判斷 5 分盤或 20 分盤
var boardType = "5分盤";
if (reason.indexOf("第二次") >= 0 || reason.indexOf("20分") >= 0) {
boardType = "20分盤";
}
// 轉換民國年(7碼)為西元日期顯示
function rocToWest(s) {
if (!s || s.length < 7) return s;
var y = parseInt(s.substring(0, 3)) + 1911;
var m = s.substring(3, 5);
var d = s.substring(5, 7);
return y + "/" + m + "/" + d;
}
var parts = period.split("~");
var endRoc = parts[1] || "";
var endWest = rocToWest(endRoc);
var periodWest = rocToWest(parts[0]) + "~" + endWest;
// 判斷是否還在處置期間
var isActive = true;
if (endRoc.length >= 7) {
isActive = parseInt(endRoc) + 19110000 >= parseInt(today);
}
if (isActive && code && name) {
otc.push({
code: code, name: name,
boardType: boardType,
period: periodWest,
});
}
});
Logger.log("上櫃處置:" + otc.length + " 檔");
}
} catch(e) { Logger.log("上櫃處置失敗:" + e); }
// ── 組合輸出文字 ──
if (listed.length === 0 && otc.length === 0) {
return "(今日無處置資料,或資料尚未更新)";
}
var lines = [];
if (listed.length > 0) {
lines.push("【上市處置中】");
listed.forEach(function(s) {
lines.push(s.code + " " + s.name + " " + s.boardType + " " + s.period);
});
}
if (otc.length > 0) {
lines.push("【上櫃處置中】");
otc.forEach(function(s) {
lines.push(s.code + " " + s.name + " " + s.boardType + " " + s.period);
});
}
lines.push("\n數據來源:臺灣證交所 / 櫃買中心官方 API");
return lines.join("\n");
}
// ══════════════════════════════════════════════════════════
// AI 數據(Claude API + web_search)
// 漲停名單由官方數據提供,Claude 只做族群分類
// ══════════════════════════════════════════════════════════
function fetchAllAiData(limitUpStocks, dateDisplay) {
const fallback = {
us_market: "(AI摘要暫時無法取得)",
tw_market: "(AI摘要暫時無法取得)",
macro: "(AI摘要暫時無法取得)",
earnings: "(AI摘要暫時無法取得)",
limit_up: "(AI摘要暫時無法取得)",
sector_rank: "(AI摘要暫時無法取得)",
disposal: "(AI摘要暫時無法取得)",
};
if (!CONFIG.ANTHROPIC_API_KEY) return fallback;
const todayStr = Utilities.formatDate(new Date(), "Asia/Taipei", "yyyy年MM月dd日");
const dDisplay = dateDisplay || "昨日";
// 建立官方漲停名單字串(代號+名稱,來源100%正確)
var limitUpList = "";
if (limitUpStocks && limitUpStocks.length > 0) {
limitUpList = limitUpStocks.map(function(s) { return s.code + " " + s.name; }).join("、");
Logger.log("官方漲停名單傳入 Claude:" + limitUpStocks.length + " 檔");
}
// 漲停 prompt:有官方名單就讓 Claude 分族群,沒有才讓它搜尋
var limitUpPrompt = "";
if (limitUpList) {
limitUpPrompt = "以下是" + dDisplay + "台股官方漲停個股(代號與名稱均來自臺灣證交所,資料正確,請勿新增或修改任何個股):\n"
+ limitUpList + "\n"
+ "請依上述名單,把個股依產業族群分類,格式:【族群名稱】代號 名稱|代號 名稱(每個族群之間換行)。只能使用我給的名單。";
} else {
limitUpPrompt = "搜尋" + dDisplay + "台股漲停個股,格式:【族群名稱】代號 名稱|代號 名稱,依族群分組,至少10檔,不確定的個股請勿列入。";
}
const prompt = "今天是 " + todayStr + "(台灣時間)。\n\n"
+ "【任務一:漲停族群分類】\n" + limitUpPrompt + "\n\n"
+ "【任務二:搜尋以下主題並整理繁體中文摘要】\n"
+ "1. us_market:昨晚美股收盤重點(S&P500/那斯達克漲跌主因、重要財報或Fed談話)(150字內)\n"
+ "2. tw_market:今日台股重點(加權指數、AI/半導體/電子供應鏈動態)(150字內)\n"
+ "3. macro:PMI最新讀值、油價趨勢(3句話:現在位置/近期走勢/對台股意義)、地緣政治(100字內)\n"
+ "4. earnings:今日及本週台股重要法說會摘要,若無則寫今日無重要法說會(100字內)\n"
+ "5. sector_rank:昨日類股漲跌幅,格式:▲強勢:族群A +X.XX%|族群B +X.XX%|族群C +X.XX%\\n▼弱勢:族群D -X.XX%|族群E -X.XX%|族群F -X.XX%\n"
+ "回傳純 JSON(第一個字是{,最後一個字是},不含任何說明或markdown):\n"
+ '{"limit_up":"族群分類結果",'
+ '"us_market":"...",'
+ '"tw_market":"...",'
+ '"macro":"...",'
+ '"earnings":"...",'
+ '"sector_rank":"...",'
+ '"sector_rank":"..."}';
try {
const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": CONFIG.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
payload: JSON.stringify({
model: "claude-sonnet-4-6",
max_tokens: 3000,
tools: [{ type: "web_search_20250305", name: "web_search" }],
messages: [{ role: "user", content: prompt }],
}),
muteHttpExceptions: true,
});
if (res.getResponseCode() === 200) {
const blocks = JSON.parse(res.getContentText()).content || [];
const text = blocks.filter(function(b) { return b.type === "text"; })
.map(function(b) { return b.text; }).join("");
Logger.log("Claude 回傳長度:" + text.length);
const match = text.match(/\{[\s\S]*\}/);
if (match) {
try {
// 清理常見 JSON 破壞字元
var cleaned = match[0]
.replace(/[\u0000-\u001F\u007F]/g, " ") // 控制字元
.replace(/,\s*}/g, "}") // 尾逗號
.replace(/,\s*]/g, "]");
var result = JSON.parse(cleaned);
// 若 Claude 沒回傳 limit_up,用官方名單直接填入
if (!result.limit_up && limitUpList) {
result.limit_up = "【漲停】" + dDisplay + " 官方名單(族群分類解析失敗):" + limitUpList;
}
return result;
} catch(e) { Logger.log("JSON 解析失敗:" + e); }
}
} else {
Logger.log("Claude API 錯誤:" + res.getResponseCode() + " " + res.getContentText().substring(0, 200));
}
} catch(e) { Logger.log("Claude API 異常:" + e); }
return fallback;
}
// ══════════════════════════════════════════════════════════
// 工具函式
// ══════════════════════════════════════════════════════════
function round(v, d) { return Math.round(v * Math.pow(10,d)) / Math.pow(10,d); }
function fmtDate(d) { return Utilities.formatDate(d, "Asia/Taipei", "yyyy/MM/dd"); }
function arrow(v) { return typeof v === "number" ? (v >= 0 ? "▲" : "▼") : "—"; }
function chgColor(v) { return typeof v === "number" ? (v >= 0 ? "#dc2626" : "#16a34a") : "#6b7280"; }
function fmtPrice(p) {
if (typeof p !== "number") return String(p);
return p < 10 ? p.toFixed(4) : p.toLocaleString("en-US", {minimumFractionDigits:2, maximumFractionDigits:2});
}
function fmtChg(c) {
if (typeof c !== "number") return "";
const a = Math.abs(c);
return a < 1 ? a.toFixed(4) : a.toLocaleString("en-US", {minimumFractionDigits:2, maximumFractionDigits:2});
}
// ══════════════════════════════════════════════════════════
// HTML 組裝
// ══════════════════════════════════════════════════════════
function buildHtml(today, fg, markets, oi, twData, ai, disposal) {
const dateStr = Utilities.formatDate(today, "Asia/Taipei", "yyyy年MM月dd日");
const wd = ["日","一","二","三","四","五","六"][today.getDay()];
const genTime = Utilities.formatDate(today, "Asia/Taipei", "yyyy-MM-dd HH:mm");
const fgv = fg.value;
const fgCol = fgColor(typeof fgv === "number" ? fgv : -1);
const oiNet = oi.net;
const oiCol = typeof oiNet === "number" ? (oiNet < 0 ? "#16a34a" : "#dc2626") : "#6b7280";
const oiNetFmt = typeof oiNet === "number" ? oiNet.toLocaleString("en-US") : String(oiNet);
const oiLabel = typeof oiNet === "number"
? (oiNet < 0 ? "淨空單(外資看空)" : "淨多單(外資看多)") : "數據待確認";
// 前值與差值
var oiPrevHtml = "";
if (oi.prevNet !== null && oi.prevNet !== undefined && oi.diff !== null && oi.diff !== undefined) {
const diffCol = oi.diff > 0 ? "#dc2626" : "#16a34a";
const diffSign = oi.diff > 0 ? "+" : "";
const diffLabel = oi.diff > 0 ? "空單回補(看空減輕)" : "空單增加(看空加重)";
oiPrevHtml = '<div style="margin-top:8px;padding-top:8px;border-top:1px solid #1e293b;">'
+ '<div style="font-size:10px;color:#64748b;margin-bottom:3px;">前值(' + oi.prevDate + '):'
+ '<span style="color:#94a3b8;font-family:\'Courier New\',monospace;">'
+ oi.prevNet.toLocaleString("en-US") + '</span></div>'
+ '<div style="font-size:10px;color:#64748b;">較前日:'
+ '<span style="color:' + diffCol + ';font-family:\'Courier New\',monospace;font-weight:700;">'
+ diffSign + oi.diff.toLocaleString("en-US") + ' 口</span>'
+ ' <span style="color:' + diffCol + ';">(' + diffLabel + ')</span>'
+ '</div>'
+ '</div>';
}
const sp = markets["^GSPC"] || {};
const nq = markets["^IXIC"] || {};
const vix = markets["^VIX"] || {};
const oil = markets["BZ=F"] || {};
function secHeader(num, title, sub) {
return '<div style="display:flex;align-items:center;gap:8px;border-bottom:2px solid #f1f5f9;padding-bottom:8px;margin-bottom:14px;">'
+ '<span style="background:#0f172a;color:#f8fafc;font-size:9px;letter-spacing:2px;padding:3px 8px;border-radius:3px;font-family:\'Courier New\',monospace;">' + num + '</span>'
+ '<span style="font-size:14px;font-weight:700;">' + title + '</span>'
+ '<span style="font-size:10px;color:#9ca3af;margin-left:auto;font-family:\'Courier New\',monospace;">' + sub + '</span>'
+ '</div>';
}
function aiBlock(label, content) {
var c = content || "(暫無資料)";
c = c.replace(/([^】])\s*【/g, "$1<br><br>【");
c = c.replace(/】/g, "】<br> ");
c = c.replace(/\s*\(([12])\)/g, "<br><br>($1)");
c = c.replace(/\s*(▲|▼)/g, "<br>$1");
c = c.replace(/\|/g, "<br> ");
c = c.replace(/^(<br>)+/, "");
return '<div style="margin-bottom:16px;">'
+ '<div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:5px;">' + label + '</div>'
+ '<div style="font-size:13px;line-height:2.1;color:#374151;background:#f8fafc;border-left:3px solid #e2e8f0;padding:11px 13px;">' + c + '</div>'
+ '</div>';
}
function tickerItem(label, val, pct, col) {
const ps = typeof pct === "number" ? "(" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)" : "";
return '<td style="padding:6px 10px;text-align:center;">'
+ '<div style="font-size:9px;color:#64748b;letter-spacing:1px;text-transform:uppercase;">' + label + '</div>'
+ '<div style="font-size:12px;font-weight:700;color:' + col + ';font-family:\'Courier New\',monospace;">'
+ fmtPrice(val) + ' <span style="font-size:9px;">' + ps + '</span></div>'
+ '</td>';
}
const mktSymbols = ["^GSPC","^IXIC","^VIX","^TNX","BZ=F","GC=F","TWD=X"];
const mktRows = mktSymbols.map(function(sym) {
const m = markets[sym] || { name: sym, price: "N/A", change: 0, pct: 0 };
const col = chgColor(m.change);
const arr = arrow(m.change);
const ps = typeof m.pct === "number" ? (m.pct >= 0 ? "+" : "") + m.pct.toFixed(2) : "—";
return '<tr>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:10px;color:#6b7280;padding:7px 10px;border-bottom:1px solid #f3f4f6;">' + sym + '</td>'
+ '<td style="font-size:12px;font-weight:600;padding:7px 10px;border-bottom:1px solid #f3f4f6;">' + m.name + '</td>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:12px;font-weight:700;padding:7px 10px;border-bottom:1px solid #f3f4f6;text-align:right;">' + fmtPrice(m.price) + '</td>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:11px;color:' + col + ';padding:7px 10px;border-bottom:1px solid #f3f4f6;text-align:right;">'
+ arr + ' ' + fmtChg(m.change) + ' (' + ps + '%)</td>'
+ '</tr>';
}).join("");
var foreignRows = "";
if (twData.foreignBuy && twData.foreignBuy.length > 0) {
twData.foreignBuy.forEach(function(r) {
const isBuy = r.type === "買超";
const col = isBuy ? "#dc2626" : "#16a34a";
const netFmt = typeof r.net === "number" ? r.net.toLocaleString("en-US") : String(r.net);
foreignRows += '<tr>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:10px;color:#6b7280;padding:6px 10px;border-bottom:1px solid #f3f4f6;">' + r.code + '</td>'
+ '<td style="font-size:12px;padding:6px 10px;border-bottom:1px solid #f3f4f6;">' + r.name + '</td>'
+ '<td style="text-align:center;padding:6px 10px;border-bottom:1px solid #f3f4f6;">'
+ '<span style="background:' + col + '22;color:' + col + ';font-size:10px;padding:1px 6px;border-radius:3px;">' + r.type + '</span></td>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:11px;color:' + col + ';text-align:right;padding:6px 10px;border-bottom:1px solid #f3f4f6;">' + netFmt + '</td>'
+ '</tr>';
});
} else {
foreignRows = '<tr><td colspan="4" style="padding:10px;text-align:center;color:#9ca3af;font-size:12px;">數據暫時無法取得</td></tr>';
}
var volumeRows = "";
if (twData.topVolume && twData.topVolume.length > 0) {
twData.topVolume.forEach(function(r) {
volumeRows += '<tr>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:10px;color:#6b7280;padding:6px 10px;border-bottom:1px solid #f3f4f6;">' + r.code + '</td>'
+ '<td style="font-size:12px;padding:6px 10px;border-bottom:1px solid #f3f4f6;">' + r.name + '</td>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:11px;text-align:right;padding:6px 10px;border-bottom:1px solid #f3f4f6;">' + r.volume + '</td>'
+ '<td style="font-family:\'Courier New\',monospace;font-size:11px;text-align:right;padding:6px 10px;border-bottom:1px solid #f3f4f6;">' + r.close + '</td>'
+ '</tr>';
});
} else {
volumeRows = '<tr><td colspan="4" style="padding:10px;text-align:center;color:#9ca3af;font-size:12px;">數據暫時無法取得</td></tr>';
}
return '<!DOCTYPE html>'
+ '<html lang="zh-TW"><head><meta charset="UTF-8">'
+ '<meta name="viewport" content="width=device-width,initial-scale=1.0">'
+ '<title>獵犬交易日報 ' + dateStr + '</title></head>'
+ '<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,\'Helvetica Neue\',Arial,sans-serif;color:#111827;">'
+ '<div style="max-width:680px;margin:0 auto;background:#fff;">'
// HEADER
+ '<div style="background:#0f172a;padding:28px 28px 20px;text-align:center;">'
+ '<div style="font-size:26px;letter-spacing:8px;margin-bottom:6px;">🐕</div>'
+ '<div style="font-size:24px;font-weight:700;color:#f8fafc;letter-spacing:6px;margin-bottom:4px;">獵犬交易日報</div>'
+ '<div style="font-size:9px;letter-spacing:4px;color:#64748b;text-transform:uppercase;margin-bottom:12px;">THE HUNTING DOG · DAILY MARKET INTELLIGENCE</div>'
+ '<div style="display:inline-block;background:#1e293b;border-radius:20px;padding:5px 16px;font-size:11px;color:#cbd5e1;letter-spacing:2px;">'
+ dateStr + ' 週' + wd + '</div>'
+ '</div>'
// BODY
+ '<div style="padding:24px;">'
// §01 市場體溫
+ '<div style="margin-bottom:24px;">'
+ secHeader("01", "市場體溫", "CNN FEAR & GREED / YAHOO FINANCE")
+ '<div style="display:flex;align-items:center;gap:18px;background:#f8fafc;border:1.5px solid ' + fgCol + '33;border-radius:10px;padding:16px 18px;margin-bottom:14px;">'
+ '<div style="width:72px;height:72px;border-radius:50%;border:3px solid ' + fgCol + ';background:#fff;flex-shrink:0;text-align:center;padding-top:14px;box-sizing:border-box;">'
+ '<div style="font-size:26px;font-weight:700;color:' + fgCol + ';font-family:\'Courier New\',monospace;line-height:1;">' + fgv + '</div>'
+ '<div style="font-size:8px;color:' + fgCol + ';">/100</div>'
+ '</div>'
+ '<div>'
+ '<div style="font-size:14px;font-weight:700;color:' + fgCol + ';margin-bottom:3px;">' + fg.label + '</div>'
+ '<div style="font-size:11px;color:#6b7280;line-height:1.7;">0–24 極度恐懼 25–44 恐懼 45–55 中性<br>56–74 貪婪 75–100 極度貪婪<br><span style="color:#9ca3af;">來源:' + fg.source + '</span></div>'
+ '</div></div>'
+ '<div style="border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;">'
+ '<table style="width:100%;border-collapse:collapse;">'
+ '<thead><tr style="background:#0f172a;">'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:left;letter-spacing:2px;font-family:\'Courier New\',monospace;font-weight:400;">代號</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:left;letter-spacing:2px;font-family:\'Courier New\',monospace;font-weight:400;">名稱</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:right;letter-spacing:2px;font-family:\'Courier New\',monospace;font-weight:400;">收盤</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:right;letter-spacing:2px;font-family:\'Courier New\',monospace;font-weight:400;">漲跌</th>'
+ '</tr></thead>'
+ '<tbody>' + mktRows + '</tbody>'
+ '</table></div>'
+ aiBlock("[美股重點]", ai.us_market)
+ '</div>'
// §02 台股籌碼雷達
+ '<div style="margin-bottom:24px;">'
+ secHeader("02", "台股籌碼雷達", "臺灣期交所 / 證交所")
+ '<div style="background:#0f172a;border-radius:10px;padding:18px 22px;margin-bottom:14px;">'
+ '<table style="width:100%;border-collapse:collapse;"><tr>'
+ '<td style="vertical-align:top;padding-right:20px;">'
+ '<div style="font-size:9px;color:#475569;letter-spacing:2px;text-transform:uppercase;margin-bottom:5px;font-family:\'Courier New\',monospace;">外資台指期淨部位(口)全月份合計</div>'
+ '<div style="font-size:36px;font-weight:700;color:' + oiCol + ';font-family:\'Courier New\',monospace;line-height:1;">' + oiNetFmt + '</div>'
+ '<div style="font-size:11px;color:' + oiCol + ';margin-top:3px;">' + oiLabel + '</div>'
+ oiPrevHtml
+ '</td>'
+ '<td style="vertical-align:top;">'
+ '<table style="border-collapse:collapse;margin-bottom:8px;"><tr>'
+ '<td style="padding-right:20px;"><div style="font-size:9px;color:#64748b;margin-bottom:2px;">多單</div>'
+ '<div style="font-size:14px;font-weight:700;color:#dc2626;font-family:\'Courier New\',monospace;">' + (typeof oi.longOI === "number" ? oi.longOI.toLocaleString("en-US") : oi.longOI) + '</div></td>'
+ '<td><div style="font-size:9px;color:#64748b;margin-bottom:2px;">空單</div>'
+ '<div style="font-size:14px;font-weight:700;color:#16a34a;font-family:\'Courier New\',monospace;">' + (typeof oi.shortOI === "number" ? oi.shortOI.toLocaleString("en-US") : oi.shortOI) + '</div></td>'
+ '</tr></table>'
+ '<div style="font-size:10px;color:#475569;">' + (oi.ok ? "✅ 臺灣期交所官方 CSV " : "⚠️ 數據暫時無法取得 ") + oi.date + '</div>'
+ '</td></tr></table>'
+ '</div>'
+ '<div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:6px;">[外資現貨] 買超前五 / 賣超前五</div>'
+ '<div style="border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;margin-bottom:14px;">'
+ '<table style="width:100%;border-collapse:collapse;">'
+ '<thead><tr style="background:#0f172a;">'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:left;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">代號</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:left;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">名稱</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:center;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">方向</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:right;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">張數</th>'
+ '</tr></thead>'
+ '<tbody>' + foreignRows + '</tbody>'
+ '</table></div>'
+ '<div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:6px;">[成交量] 當日前五名</div>'
+ '<div style="border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;">'
+ '<table style="width:100%;border-collapse:collapse;">'
+ '<thead><tr style="background:#0f172a;">'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:left;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">代號</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:left;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">名稱</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:right;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">成交張數</th>'
+ '<th style="font-size:9px;color:#94a3b8;padding:6px 10px;text-align:right;font-family:\'Courier New\',monospace;font-weight:400;letter-spacing:1px;">收盤價</th>'
+ '</tr></thead>'
+ '<tbody>' + volumeRows + '</tbody>'
+ '</table></div>'
+ '</div>'
// §03 資金流向偵測
+ '<div style="margin-bottom:24px;">'
+ secHeader("03", "資金流向偵測", "臺灣證交所官方數據 + CLAUDE AI 族群分類")
+ aiBlock("[漲停個股+族群]", ai.limit_up)
+ aiBlock("[類股漲跌幅排行]", ai.sector_rank)
+ '</div>'
// §04 處置股雷達
+ '<div style="margin-bottom:24px;">'
+ secHeader("04", "處置股雷達", "臺灣證交所 / 櫃買中心官方 API")
+ '<div style="background:#fefce8;border:1.5px solid #ca8a04;border-radius:8px;padding:14px 16px;">'
+ '<div style="font-size:13px;line-height:1.9;color:#374151;white-space:pre-line;">' + (disposal || "(暫無資料)") + '</div>'
+ '</div>'
+ '</div>'
// §05 總經雷達
+ '<div style="margin-bottom:24px;">'
+ secHeader("05", "總經雷達", "PMI / 油價 / 地緣政治")
+ aiBlock("", ai.macro)
+ '</div>'
// §06 產業訊號與法說
+ '<div style="margin-bottom:16px;">'
+ secHeader("06", "產業訊號 & 法說會", "CLAUDE AI + WEB SEARCH")
+ aiBlock("[台股產業動態]", ai.tw_market)
+ aiBlock("[法說會重點]", ai.earnings)
+ '</div>'
+ '</div>'
// FOOTER
+ '<div style="background:#f8fafc;border-top:1px solid #e5e7eb;padding:16px 24px;text-align:center;">'
+ '<div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:4px;">🐕 獵犬交易日報 v3.1</div>'
+ '<div style="font-size:10px;color:#9ca3af;line-height:1.8;">'
+ '數據來源:Yahoo Finance · 臺灣期交所 · 臺灣證交所 · CNN Fear & Greed · Anthropic Claude<br>'
+ '漲停個股代號與名稱來自臺灣證交所官方數據 · 本報告不構成投資建議<br>'
+ '自動生成:' + genTime + ' (Asia/Taipei)'
+ '</div></div>'
+ '</div></body></html>';
}
// ══════════════════════════════════════════════════════════
// 測試函式
// ══════════════════════════════════════════════════════════
function testApiKey() {
const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": CONFIG.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
payload: JSON.stringify({
model: "claude-haiku-4-5-20251001",
max_tokens: 10,
messages: [{ role: "user", content: "hi" }],
}),
muteHttpExceptions: true,
});
Logger.log(res.getResponseCode() + " " + res.getContentText());
}
function checkKey() {
Logger.log("Key 長度:" + CONFIG.ANTHROPIC_API_KEY.length);
Logger.log("Key 開頭:" + CONFIG.ANTHROPIC_API_KEY.substring(0, 15));
}
function testTaifexCols() {
const dateStr = "2026/07/08";
const url = "https://www.taifex.com.tw/cht/3/futContractsDateDown"
+ "?queryStartDate=" + dateStr + "&queryEndDate=" + dateStr + "&commodity_id=TX";
const res = UrlFetchApp.fetch(url, {
headers: { "User-Agent": "Mozilla/5.0", "Referer": "https://www.taifex.com.tw/" },
muteHttpExceptions: true,
});
const bytes = res.getContent();
const text = Utilities.newBlob(bytes).setContentType("text/plain; charset=big5").getDataAsString();
const rows = text.split("\n").map(function(r) {
return r.split(",").map(function(c) { return c.replace(/"/g,"").trim(); });
});
// 印出第 1–10 行的所有欄位(跳過 header 第0行)
for (var i = 1; i <= 10; i++) {
if (!rows[i]) continue;
Logger.log("=== 行" + i + " (共" + rows[i].length + "欄) ===");
rows[i].forEach(function(col, j) {
Logger.log(" [" + j + "] = [" + col + "]");
});
}
}
function testTaifexAllMonths() {
const dateStr = "2026/07/09";
// 三大法人-臺股期貨各月份合約未平倉
const url = "https://www.taifex.com.tw/cht/3/futContractsDateDown"
+ "?queryStartDate=" + dateStr + "&queryEndDate=" + dateStr
+ "&commodity_id=TXF"; // TXF = 臺股期貨(含所有月份)
const res = UrlFetchApp.fetch(url, {
headers: { "User-Agent": "Mozilla/5.0", "Referer": "https://www.taifex.com.tw/" },
muteHttpExceptions: true,
});
const bytes = res.getContent();
const text = Utilities.newBlob(bytes).setContentType("text/plain; charset=big5").getDataAsString();
const rows = text.split("\n").map(function(r) {
return r.split(",").map(function(c) { return c.replace(/"/g,"").trim(); });
});
Logger.log("回應碼:" + res.getResponseCode() + " 共 " + rows.length + " 行");
// 印出所有行
rows.forEach(function(row, i) {
if (i < 20) Logger.log("行" + i + ":" + JSON.stringify(row));
});
}
function testDisposalPages() {
// 上市處置
try {
const r1 = UrlFetchApp.fetch("https://www.twse.com.tw/zh/announcement/punish.html", {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
Logger.log("上市 回應碼:" + r1.getResponseCode());
Logger.log("上市 前500字:" + r1.getContentText().substring(0, 500));
} catch(e) { Logger.log("上市 失敗:" + e); }
// 上市處置 API 版本
try {
const r2 = UrlFetchApp.fetch("https://www.twse.com.tw/rwd/zh/announcement/punish?response=json", {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
Logger.log("上市API 回應碼:" + r2.getResponseCode());
Logger.log("上市API 前500字:" + r2.getContentText().substring(0, 500));
} catch(e) { Logger.log("上市API 失敗:" + e); }
// 上櫃處置
try {
const r3 = UrlFetchApp.fetch("https://www.tpex.org.tw/zh-tw/announce/market/disposal.html", {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
Logger.log("上櫃 回應碼:" + r3.getResponseCode());
Logger.log("上櫃 前500字:" + r3.getContentText().substring(0, 500));
} catch(e) { Logger.log("上櫃 失敗:" + e); }
}
function testTpexDisposal() {
const urls = [
"https://www.tpex.org.tw/web/bulletin/disposal/disposal_list.php?l=zh-tw&o=json",
"https://www.tpex.org.tw/openapi/v1/tpex_disposal_information",
"https://www.tpex.org.tw/web/bulletin/disposal/disposal_list.php?o=json",
];
urls.forEach(function(url) {
try {
const r = UrlFetchApp.fetch(url, {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
Logger.log(url);
Logger.log("回應碼:" + r.getResponseCode() + " 前300字:" + r.getContentText().substring(0, 300));
} catch(e) { Logger.log(url + " 失敗:" + e); }
});
}
function testMacroMicro() {
// 財經M平方有提供 CNN F&G 的 API 數據
const urls = [
"https://en.macromicro.me/charts/50108/cnn-fear-and-greed",
"https://www.macromicro.me/api/v1/charts/50108/data?type=chart",
"https://www.macromicro.me/collections/10/cc-topcharts/50108/cnn-fear-and-greed",
];
urls.forEach(function(url) {
try {
const r = UrlFetchApp.fetch(url, {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
Logger.log(url);
Logger.log("回應碼:" + r.getResponseCode() + " 前300字:" + r.getContentText().substring(0, 300));
} catch(e) { Logger.log(url + " 失敗:" + e); }
});
}
function testFinhacker() {
const urls = [
"https://finhacker.cz/en/fear-and-greed-index-historical-data-and-chart/",
"https://finhacker.cz/api/fear-greed/",
"https://finhacker.cz/fear-greed-index/",
];
urls.forEach(function(url) {
try {
const r = UrlFetchApp.fetch(url, {
headers: { "User-Agent": "Mozilla/5.0" },
muteHttpExceptions: true,
});
Logger.log(url);
Logger.log("回應碼:" + r.getResponseCode() + " 前400字:" + r.getContentText().substring(0, 400));
} catch(e) { Logger.log(url + " 失敗:" + e); }
});
}
function testFinhackerParse() {
const r = UrlFetchApp.fetch(
"https://finhacker.cz/en/fear-and-greed-index-historical-data-and-chart/",
{ headers: { "User-Agent": "Mozilla/5.0" }, muteHttpExceptions: true }
);
const text = r.getContentText();
// 找 "current" 或今天日期附近的數值
const today = Utilities.formatDate(new Date(), "America/New_York", "yyyy-MM-dd");
const idx = text.indexOf(today);
Logger.log("今天日期 " + today + " 出現位置:" + idx);
if (idx >= 0) {
Logger.log("附近內容:" + text.substring(idx - 50, idx + 100));
}
// 找常見的數值標記
const patterns = ['"value":', '"score":', '"fgi":', 'current_value', 'fng_value'];
patterns.forEach(function(p) {
const i = text.indexOf(p);
if (i >= 0) Logger.log("找到 " + p + ":" + text.substring(i, i + 80));
});
Logger.log("HTML 總長度:" + text.length);
}
0 Comments
張貼留言