Leveraged ETF Rebalancer
槓桿部位再平衡試算工具 — 設定目標比例,即時算出建議買賣張數與整體曝險(Beta)。
目前曝險 Beta
Beta(曝險倍數)= 大盤漲跌 1% 時,你整個資產大約跟著變動幾 %。 Beta 1.4 就是大盤漲 1%、你賺約 1.4%;大盤跌 1%、你也賠約 1.4%。
怎麼算的:把每個部位的「佔比 × 槓桿倍數」加起來。 槓桿倍數依代號自動判斷 —— 結尾是 L 的正 2 ETF 算 2 倍,一般股票 1 倍, 現金 0 倍(放現金不會跟著大盤動,這就是現金能壓低曝險的原因)。
例:00631L 30% + 00685L 40% + 現金 30%
→ 0.30×2 + 0.40×2 + 0.30×0 = 1.40
設計 Beta= 你想維持的曝險水準,也就是這套策略的核心設定。 調高=更積極、波動更大;調低=更保守。
再平衡偏移 %= 容許實際偏離設計值多少才動作。 設 10%、設計值 1.4,容許範圍就是 1.26 ~ 1.54。 設太小會頻繁交易增加成本,設太大則失去控制曝險的意義。
為什麼要再平衡:股票漲的時候,它佔資產的比例自動變大, 曝險跟著升高——等於在高點承擔更多風險。再平衡就是把它拉回設定值。
簡單說:這是一套機械化的「漲了賣、跌了買」紀律, 靠固定的曝險目標決定何時動作,不用預測行情。
資產總覽
策略檢視
| 大盤 | 你的資產 | 帳面損益 | 加碼所需 | 現金 |
|---|
部位變化
交易與現金紀錄
刪除一筆會反向沖銷,並重算之後所有紀錄| 時間 | 項目 | 類別 | 張數 | 價格 | 總金額 | 已實現損益 | 異動後股數 | 異動後現金 |
|---|
配置紀錄
在「試算」頁按「儲存目前配置」新增快照| 時間 | 目標比例 | 設計 Beta | 偏移 % | 當下 Beta |
|---|
跨裝置同步(Firebase)
{
"rules": {
"你的路徑": {
".read": true,
".write": true
}
}
}
資料節點是一組隨機字串,等於你的通行證。規則設定成只有這個節點可以讀寫, 其他路徑一律拒絕,所以別人即使知道資料庫網址,猜不到節點就進不來。 這不是加密保護,請不要把節點字串貼給別人或公開。
Google 試算表同步
var SCRIPT_VERSION = 'v1.9';
function doPost(e) {
var data = JSON.parse(e.postData.contents);
var ss = SpreadsheetApp.getActiveSpreadsheet();
writeSheet(ss, '交易紀錄',
['時間','項目','類別','張數','價格','總金額','異動後股數','異動後現金'],
data.trades || []);
writeSheet(ss, '配置紀錄',
['時間','目標比例','設計Beta','偏移%','當下Beta'],
data.configs || []);
writeSheet(ss, '持股現況',
['名稱','股數','現價','槓桿','現值','目標比例'],
data.holdings || []);
return ContentService
.createTextOutput(JSON.stringify({ok: true, at: new Date().toISOString()}))
.setMimeType(ContentService.MimeType.JSON);
}
function doGet(e) {
var action = e && e.parameter ? e.parameter.action : '';
// 報價中轉:瀏覽器不能直連證交所(CORS),改由這裡代抓
if (action === 'quote') {
var ex = (e.parameter.ex || '');
var url = 'https://mis.twse.com.tw/stock/api/getStockInfo.jsp?ex_ch='
+ encodeURIComponent(ex) + '&json=1&delay=0';
var out;
try {
var res = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
headers: {
'User-Agent': 'Mozilla/5.0',
'Referer': 'https://mis.twse.com.tw/stock/index.jsp'
}
});
out = res.getContentText();
JSON.parse(out); // 確認是合法 JSON,不是錯誤頁
} catch (err) {
out = JSON.stringify({ msgArray: [], error: String(err) });
}
return ContentService.createTextOutput(out)
.setMimeType(ContentService.MimeType.JSON);
}
// 版本探測:不抓任何外部資料,只回報這份程式碼支援哪些功能
if (action === 'ping') {
return ContentService.createTextOutput(JSON.stringify({
ok: true, version: SCRIPT_VERSION, features: ['quote', 'macro', 'level']
})).setMimeType(ContentService.MimeType.JSON);
}
// 總經參考:抓證交所 OpenAPI,算好摘要再回傳(避免傳整包上千筆)
if (action === 'macro') {
try {
var out = {};
var idx = JSON.parse(UrlFetchApp.fetch(
'https://openapi.twse.com.tw/v1/exchangeReport/MI_INDEX',
{ muteHttpExceptions: true }).getContentText());
for (var i = 0; i < idx.length; i++) {
if (idx[i]['指數'] === '發行量加權股價指數') {
var sign = (idx[i]['漲跌'] === '-') ? -1 : 1;
out.taiex = {
close: idx[i]['收盤指數'],
changePct: sign * parseFloat(idx[i]['漲跌百分比'] || 0)
};
break;
}
}
var bw = JSON.parse(UrlFetchApp.fetch(
'https://openapi.twse.com.tw/v1/exchangeReport/BWIBBU_ALL',
{ muteHttpExceptions: true }).getContentText());
var pe = [], pb = [], dy = [];
for (var j = 0; j < bw.length; j++) {
var a = parseFloat(bw[j].PEratio), b = parseFloat(bw[j].PBratio), c = parseFloat(bw[j].DividendYield);
if (a > 0) pe.push(a);
if (b > 0) pb.push(b);
if (c > 0) dy.push(c);
}
out.pe = median(pe);
out.pb = median(pb);
out.yield = median(dy);
out.count = bw.length;
// level=0 代表前端已有資料,略過這段(抓 60 個月很慢)
if (e.parameter.level !== '0') {
out.level = marketLevel(parseInt(e.parameter.months || '60', 10));
}
return ContentService.createTextOutput(JSON.stringify(out))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ error: String(err) }))
.setMimeType(ContentService.MimeType.JSON);
}
}
return ContentService
.createTextOutput(JSON.stringify({ok: true, msg: '連線正常'}))
.setMimeType(ContentService.MimeType.JSON);
}
// 0050 收盤序列。2025-06-18 一拆四,之前的價格要除以 4 才能跟現在比較
var SPLITS_0050 = [{ date: '20250618', ratio: 4 }];
var HIST_SHEET = '0050歷史';
// 抓過的月份存進試算表,之後只補缺的月份(5 年 = 60 個月,每次重抓太慢)
function marketLevel(months) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName(HIST_SHEET);
if (!sh) {
sh = ss.insertSheet(HIST_SHEET);
sh.getRange(1, 1, 1, 2).setValues([['日期', '收盤價(原始)']]).setFontWeight('bold');
sh.setFrozenRows(1);
}
// 日期欄一定要是純文字,否則試算表會把 2022-09-28 轉成日期物件,排序與分割還原都會壞掉
sh.getRange('A:A').setNumberFormat('@');
var have = {}, rows = [];
var last = sh.getLastRow();
if (last > 1) {
var vals = sh.getRange(2, 1, last - 1, 2).getValues();
for (var i = 0; i < vals.length; i++) {
var d = toIso(vals[i][0]), c = parseFloat(vals[i][1]);
if (d && c > 0) { have[d] = c; rows.push([d, c]); }
}
}
var now = new Date();
var thisYM = now.getFullYear() + ('0' + (now.getMonth() + 1)).slice(-2);
var monthsHeld = {};
for (var k in have) monthsHeld[k.slice(0, 4) + k.slice(5, 7)] = true;
var added = 0;
for (var m = months - 1; m >= 0; m--) {
var dt = new Date(now.getFullYear(), now.getMonth() - m, 1);
var ym = dt.getFullYear() + ('0' + (dt.getMonth() + 1)).slice(-2);
// 已抓過且不是當月就跳過;當月每次都更新
if (monthsHeld[ym] && ym !== thisYM) continue;
try {
var url = 'https://www.twse.com.tw/exchangeReport/STOCK_DAY?response=json&date='
+ ym + '01&stockNo=0050';
var r = JSON.parse(UrlFetchApp.fetch(url, { muteHttpExceptions: true }).getContentText());
if (r && r.data) {
for (var j = 0; j < r.data.length; j++) {
var roc = r.data[j][0].split('/');
var iso = (parseInt(roc[0], 10) + 1911) + '-' + roc[1] + '-' + roc[2];
var close = parseFloat(String(r.data[j][6]).replace(/,/g, ''));
if (close > 0 && !have[iso]) { have[iso] = close; rows.push([iso, close]); added++; }
}
}
Utilities.sleep(250);
} catch (err) { /* 略過抓不到的月份 */ }
}
rows.sort(function (a, b) { return a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0); });
if (added > 0) {
sh.getRange(2, 1, Math.max(rows.length, 1), 2).setValues(rows);
}
// 只取要求的區間,並做分割還原
var cutoff = new Date(now.getFullYear(), now.getMonth() - months + 1, 1);
var cutIso = cutoff.getFullYear() + '-' + ('0' + (cutoff.getMonth() + 1)).slice(-2) + '-01';
var series = [];
for (var n = 0; n < rows.length; n++) {
if (rows[n][0] >= cutIso) series.push({ d: rows[n][0], c: adjust(rows[n][1], rows[n][0]) });
}
if (!series.length) return null;
var high = series[0].c, highDate = series[0].d;
for (var q = 1; q < series.length; q++) {
if (series[q].c > high) { high = series[q].c; highDate = series[q].d; }
}
var lastPt = series[series.length - 1];
return {
series: series, high: high, highDate: highDate,
current: lastPt.c, currentDate: lastPt.d, months: months, cached: rows.length
};
}
// 試算表可能回傳 Date 物件,統一轉回 yyyy-MM-dd 字串
function toIso(v) {
if (v instanceof Date) return Utilities.formatDate(v, 'Asia/Taipei', 'yyyy-MM-dd');
var t = String(v).trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(t)) return t;
var d = new Date(t);
if (!isNaN(d.getTime())) return Utilities.formatDate(d, 'Asia/Taipei', 'yyyy-MM-dd');
return '';
}
// 把分割日之前的價格換算成現在的計價基礎
function adjust(price, iso) {
var ymd = iso.replace(/-/g, '');
for (var i = 0; i < SPLITS_0050.length; i++) {
if (ymd < SPLITS_0050[i].date) price = price / SPLITS_0050[i].ratio;
}
return Math.round(price * 100) / 100;
}
function median(arr) {
if (!arr.length) return null;
arr.sort(function (x, y) { return x - y; });
var m = Math.floor(arr.length / 2);
var v = (arr.length % 2) ? arr[m] : (arr[m - 1] + arr[m]) / 2;
return Math.round(v * 100) / 100;
}
function writeSheet(ss, name, header, rows) {
var sh = ss.getSheetByName(name) || ss.insertSheet(name);
sh.clear();
sh.getRange(1, 1, 1, header.length).setValues([header]).setFontWeight('bold');
if (rows.length) {
sh.getRange(2, 1, rows.length, header.length).setValues(rows);
}
sh.setFrozenRows(1);
}
同步為單向:網頁 → 試算表。試算表上手動修改不會回傳到網頁,每次同步會覆寫這三個分頁的內容。
資料備份
資料存在這台裝置的瀏覽器裡,換裝置、換瀏覽器或清除瀏覽資料都會不見。 重要的話請定期匯出備份,或設定上方的 Google 試算表同步。
網頁版本 Apps Script 狀態未檢查
說明:調整任一標的的目標比例時,現金目標會自動吸收差額,維持合計 100%。 「建議」以 1 張 = 1,000 股無條件捨去換算,除不盡的部分列為零股供你自行決定是否補足。 「槓桿倍數」依代號自動判斷:結尾為 L 的槓桿型 ETF 視為 2 倍,其餘視為 1 倍,現金為 0,用於計算目前 Beta 值。買入與賣出會自動連動現金部位。 此工具僅為個人資產配置試算,並非投資建議,請自行核對交易、稅費與券商對帳單。