隱私權政策
生效日期:2026-XX-XX(上架日確認前為草稿,上架前將請法務 review)
1. 適用範圍
本政策說明 AllocLab(以下簡稱「本平台」)如何蒐集、使用及保護您在使用本平台
服務時所提供或產生的個人資料。本政策適用於本平台所有功能,包含封閉測試期間的
Beta 申請及正式訂閱服務。未滿 18 歲者不得使用本平台。
2. 蒐集範圍
本平台可能蒐集以下類型的資料:
- 帳號資料:電子郵件地址、姓名(選填)、密碼(以 bcrypt 雜湊儲存,不保留明文)
- 訂閱資料:訂閱方案、訂閱起迄日、付款狀態(不含完整信用卡號)
- 研究行為:自選清單(Watchlist)、研究筆記、AI 問答互動記錄、選股條件設定、回測參數、thumbs 評分回饋
- 技術資料:IP 位址、瀏覽器類型、存取時間(用於安全防護及服務除錯)
- Cookie 同意記錄:您對各類 Cookie 的同意狀態與時間(見第 7 節)
本平台不蒐集金融帳號、身分證號、完整信用卡號等高敏感財務身份資料。
3. 資料使用目的
- 提供 AI 研究服務及個人化功能(快速卡、每日 Brief、個人化建議)
- 履行訂閱合約(方案驗證、付款處理、發票)
- 改善服務品質(分析使用行為、排除技術問題)
- 發送服務相關通知(功能更新、訂閱到期提醒、Telegram 推播)
- 防範濫用及未授權存取
本平台不會將您的個人資料出售予第三方,亦不用於廣告投放目的。
4. 第三方服務
本平台使用以下第三方服務,其隱私政策各自獨立:
-
OpenAI:提供 AI 推論(GPT 模型)。您的研究查詢可能傳送至 OpenAI API
進行處理。OpenAI 依其服務條款承諾 API 呼叫資料不用於訓練 OpenAI 自家模型。
-
Sentry:錯誤監控與效能追蹤,僅記錄技術錯誤,不含可識別個人身份的內容(無 PII)。
-
Google Analytics 4(GA4):網站使用分析(頁面瀏覽、事件),
需 Cookie 同意後啟用(見第 7 節)。
-
Telegram:推播通知服務(可選功能,由用戶主動綁定後啟用)。
-
Railway:雲端部署與資料庫服務(伺服器基礎設施)。
-
Cloudflare:CDN 與 DDoS 防護。
5. 資料保留期間
個人資料保留於訂閱有效期間,訂閱結束後再保留最長 6 個月(依個人資料保護法上限)。
您亦可隨時依第 6 節行使刪除權,提前終止保留。稽核日誌依法規要求可能有不同保留期限。
6. 您的個資權利(個資法 §3 §11)
依中華民國個人資料保護法,您對本平台所持有的個人資料享有以下權利:
- 查詢 / 閱覽:請求查閱本平台持有的您的個人資料
- 更正:請求更正不正確的個人資料
- 匯出:透過平台「帳號設定」→「匯出個人資料」,或呼叫
GET /api/user/me/export,下載 JSON 格式個資檔案
- 刪除:透過平台「帳號設定」→「刪除帳號」,或呼叫
DELETE /api/user/me,刪除帳號及所有相關資料
- 停止蒐集 / 利用:聯絡本平台提出停止特定資料使用的請求
如需行使上述權利或有任何個資疑問,請聯絡:
support@alloclab.tw。
本平台將於 15 個工作日內處理您的請求。
7. Cookie 使用
本平台使用 Cookie 及瀏覽器本地儲存(localStorage)。詳細說明請見
Cookie 政策。
您可隨時在 帳號設定 調整 Cookie 同意選項。
8. 資料安全
- 資料儲存於具安全防護之雲端伺服器(Railway 平台),位於受管理的資料中心
- 密碼以 bcrypt 雜湊加密儲存,不保留明文
- 傳輸過程採用 HTTPS 加密
- 定期 DB 備份,備份加密後存於 Cloudflare R2
- 本平台採用合理之技術措施保護資料,但無法保證絕對安全
9. 未成年人保護
本平台不對未滿 18 歲之人提供服務。若您未滿 18 歲,請勿使用本平台、
亦請勿提供任何個人資料。若本平台發現已蒐集到未成年人之個人資料,
將儘速刪除相關資料。
10. 政策修訂
本平台保留隨時修訂本政策之權利。修訂後將更新頁面頂部之「生效日期」。
重大變更將於 30 日前透過 email 及站內公告通知用戶。
繼續使用本平台即表示您接受修訂後之政策。準據法:中華民國(台灣)法律。
如有任何疑問,請聯絡
support@alloclab.tw。
window.AllocLab = window.AllocLab || {};
AllocLab.initFancyChart = function (containerId, opts) {
const el = document.getElementById(containerId);
if (!el || !window.LightweightCharts) return null;
const data = (opts && opts.data) || [];
if (data.length < 2) return null;
const LW = window.LightweightCharts;
const mode = opts.mode || 'apple';
const volume = opts.volume || null;
const support = Array.isArray(opts.support) ? opts.support : [];
const resistance = Array.isArray(opts.resistance) ? opts.resistance : [];
const unit = opts.unit || 'NT$';
const rangesRoot = opts.rangesRoot || el.parentElement;
const legendEl = opts.legendEl || (el.parentElement && el.parentElement.querySelector('.al-chart__legend-value'));
const legendDateEl = opts.legendDateEl || (el.parentElement && el.parentElement.querySelector('.al-chart__legend-date'));
const tipEl = opts.tooltipEl || (el.parentElement && el.parentElement.querySelector('.al-chart__tooltip'));
const first = data[0].value;
const last = data[data.length - 1].value;
const isUp = last >= first;
const mainColor = isUp ? '#c0392b' : '#2a7a50';
const mainTop = isUp ? 'rgba(192,57,43,0.22)' : 'rgba(42,122,80,0.22)';
const ghostTop = isUp ? 'rgba(192,57,43,0.04)' : 'rgba(42,122,80,0.04)';
const ghostLine = isUp ? 'rgba(192,57,43,0.25)' : 'rgba(42,122,80,0.25)';
const chart = LW.createChart(el, {
autoSize: true,
layout: {
background: { type: 'solid', color: '#FFFFFF' },
textColor: 'rgba(39,48,67,0.50)',
fontFamily: 'IBM Plex Mono, ui-monospace, monospace',
},
grid: {
vertLines: { color: 'rgba(39,48,67,0.04)' },
horzLines: { color: 'rgba(39,48,67,0.04)' },
},
crosshair: {
mode: LW.CrosshairMode.Normal,
vertLine: { color: 'rgba(39,48,67,0.25)', width: 1, labelBackgroundColor: mainColor },
horzLine: { color: 'rgba(39,48,67,0.15)', width: 1, labelBackgroundColor: mainColor },
},
rightPriceScale: { borderColor: 'rgba(39,48,67,0.10)' },
timeScale: { borderColor: 'rgba(39,48,67,0.10)', timeVisible: true, fixRightEdge: true },
handleScroll: false,
handleScale: false,
});
// 底圖(ghost)— Apple 模式時整條都畫,但色極淡
const ghost = chart.addAreaSeries({
lineColor: ghostLine,
topColor: ghostTop,
bottomColor: 'rgba(0,0,0,0)',
lineWidth: 2,
priceLineVisible: false,
lastValueVisible: false,
crosshairMarkerVisible: false,
});
ghost.setData(data);
// 高亮(active)— 游標前鮮明、游標後在 apple 模式下會被 setData 截短
const active = chart.addAreaSeries({
lineColor: mainColor,
topColor: mainTop,
bottomColor: 'rgba(0,0,0,0)',
lineWidth: 2,
priceLineVisible: false,
lastValueVisible: false,
crosshairMarkerRadius: 5,
crosshairMarkerBorderColor: '#ffffff',
crosshairMarkerBackgroundColor: mainColor,
});
active.setData(data);
// 成交量 histogram(選配)
let volSeries = null;
if (volume && volume.length) {
volSeries = chart.addHistogramSeries({
priceFormat: { type: 'volume' },
priceScaleId: '',
scaleMargins: { top: 0.82, bottom: 0 },
});
volSeries.setData(volume.map(function (v) {
return { time: v.time, value: v.value, color: 'rgba(39,48,67,0.18)' };
}));
}
// 支撐 / 壓力線(AI 或人工標記)
// 台灣慣例:壓力(resistance)偏多頂 → 紅;支撐(support)偏空底 → 綠
const priceLines = [];
support.forEach(function (p) {
priceLines.push(active.createPriceLine({
price: p,
color: '#2a7a50',
lineWidth: 1,
lineStyle: LW.LineStyle.Dashed,
axisLabelVisible: true,
title: '支撐',
}));
});
resistance.forEach(function (p) {
priceLines.push(active.createPriceLine({
price: p,
color: '#c0392b',
lineWidth: 1,
lineStyle: LW.LineStyle.Dashed,
axisLabelVisible: true,
title: '壓力',
}));
});
// Legend & tooltip 格式化
function fmtVal(v) {
if (v == null || isNaN(v)) return '—';
return unit + ' ' + Number(v).toLocaleString('zh-TW', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function setLegend(time, value) {
if (legendEl) legendEl.textContent = fmtVal(value);
if (legendDateEl) legendDateEl.textContent = time || '';
}
setLegend(data[data.length - 1].time, last);
// Apple 高亮 + tooltip
const byTime = new Map(data.map(function (d, i) { return [d.time, i]; }));
chart.subscribeCrosshairMove(function (param) {
if (!param.time || !param.point || param.point.x < 0 || param.point.y < 0) {
if (mode === 'apple') active.setData(data);
setLegend(data[data.length - 1].time, last);
if (tipEl) tipEl.style.display = 'none';
return;
}
const idx = byTime.get(param.time);
if (idx == null) return;
const d = data[idx];
if (mode === 'apple') active.setData(data.slice(0, idx + 1));
setLegend(d.time, d.value);
if (tipEl) {
tipEl.style.display = 'block';
tipEl.innerHTML =
'