【完整製作教學】不會程式也能用 Claude 做出專屬交易日報

去年我一直有個困擾:每天早上開盤前,要自己去查台指期夜盤收盤、看外資期貨未平倉空單、找漲停族群、確認有沒有處置股……光是蒐集資料就要花 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 APIAI 只負責整理「文字類」的新聞摘要、漲停族群分類、處置股資訊。


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 漲停查詢


  // 方案BSTOCK_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/03115/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/03115/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", "yyyyMMdd");

  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. macroPMI最新讀值、油價趨勢(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", "yyyyMMdd");

  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>&nbsp;&nbsp;&nbsp;&nbsp;");

    c = c.replace(/\s*\(([12])\)/g, "<br><br>($1)");

    c = c.replace(/\s*(▲|▼)/g, "<br>$1");

    c = c.replace(/\|/g, "<br>&nbsp;&nbsp;&nbsp;&nbsp;");

    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;">&#x1F415;</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 &middot; 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 + ' &nbsp; ' + wd + '</div>'

    + '</div>'


    // BODY

    + '<div style="padding:24px;">'


    // §01 市場體溫

    + '<div style="margin-bottom:24px;">'

    + secHeader("01", "市場體溫", "CNN FEAR &amp; 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 極度恐懼&emsp;25–44 恐懼&emsp;45–55 中性<br>56–74 貪婪&emsp;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 ? "&#x2705; 臺灣期交所官方 CSV &nbsp; " : "&#x26A0;&#xFE0F; 數據暫時無法取得 &nbsp; ") + 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", "產業訊號 &amp; 法說會", "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;">&#x1F415; 獵犬交易日報 v3.1</div>'

    + '<div style="font-size:10px;color:#9ca3af;line-height:1.8;">'

    + '數據來源:Yahoo Finance &middot; 臺灣期交所 &middot; 臺灣證交所 &middot; CNN Fear &amp; Greed &middot; Anthropic Claude<br>'

    + '漲停個股代號與名稱來自臺灣證交所官方數據 &middot; 本報告不構成投資建議<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

聯絡表單