AllocLab Asset Research Workspace
更新紀錄

隱私權政策

生效日期: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 = '' + d.time + '' + '' + fmtVal(d.value) + ''; const bodyRect = el.getBoundingClientRect(); const parentRect = el.parentElement.getBoundingClientRect(); const xOffset = param.point.x + (bodyRect.left - parentRect.left); const yOffset = param.point.y + (bodyRect.top - parentRect.top); const tw = tipEl.offsetWidth; let left = xOffset + 12; if (left + tw > parentRect.width) left = xOffset - tw - 12; tipEl.style.left = left + 'px'; tipEl.style.top = Math.max(0, yOffset - 32) + 'px'; } }); el.addEventListener('mouseleave', function () { if (mode === 'apple') active.setData(data); setLegend(data[data.length - 1].time, last); if (tipEl) tipEl.style.display = 'none'; }); // Range selector(sibling `.al-chart__ranges` 內 button[data-range]) const RANGE_DAYS = { '1M': 30, '3M': 90, '6M': 180, 'YTD': null, '1Y': 365, '2Y': 730, '5Y': 1825, 'MAX': null }; function applyRange(key) { if (!rangesRoot) return; rangesRoot.querySelectorAll('[data-range]').forEach(function (b) { const active = b.dataset.range === key; b.classList.toggle('is-active', active); b.setAttribute('aria-selected', active ? 'true' : 'false'); }); const lastDate = new Date(data[data.length - 1].time); if (key === 'MAX') { chart.timeScale().fitContent(); return; } let from; if (key === 'YTD') { from = new Date(lastDate.getFullYear(), 0, 1); } else { const days = RANGE_DAYS[key]; if (days == null) { chart.timeScale().fitContent(); return; } from = new Date(lastDate); from.setDate(from.getDate() - days); } const firstDate = new Date(data[0].time); if (from < firstDate) from = firstDate; chart.timeScale().setVisibleRange({ from: from.toISOString().slice(0, 10), to: lastDate.toISOString().slice(0, 10), }); } if (rangesRoot) { rangesRoot.querySelectorAll('[data-range]').forEach(function (btn) { btn.addEventListener('click', function () { applyRange(btn.dataset.range); }); }); const initActive = rangesRoot.querySelector('[data-range].is-active'); if (initActive) applyRange(initActive.dataset.range); } return { chart: chart, active: active, ghost: ghost, vol: volSeries, priceLines: priceLines, setRange: applyRange, /* ── F4 SR Chart:setSupport / setResistance(Option B,2026-04-19)───── accepts either: (a) legacy flat array [price, price, ...] (backward compat) (b) metadata array [{price, scale, source, title?}, ...] scale → lineWidth / lineStyle: short → width 1 / Dashed mid → width 1 / Solid long → width 2 / Solid (default / unknown) → width 1 / Dashed round 2 bug fix: 清除責任移至 clearSR(),caller 在重畫前統一呼叫。 setSupport / setResistance 各自只負責 push,不清除。 否則若 caller 先呼叫 setSupport 後呼叫 setResistance, setResistance 清掉 priceLines 就會把支撐線全刪,只剩壓力線。 ──────────────────────────────────────────────────────────────────────── */ clearSR: function () { // 供 caller 在重畫前統一清除所有 SR price lines priceLines.forEach(function (l) { active.removePriceLine(l); }); priceLines.length = 0; }, setSupport: function (levels) { // 不清除(清除責任已移至 clearSR,避免 setResistance 把 setSupport 線也刪掉) (levels || []).forEach(function (item) { var price, lw, ls, title; if (item !== null && typeof item === 'object') { price = item.price; var scale = item.scale || ''; if (scale === 'long') { lw = 2; ls = LW.LineStyle.Solid; } else if (scale === 'mid') { lw = 1; ls = LW.LineStyle.Solid; } else { lw = 1; ls = LW.LineStyle.Dashed; } title = item.title != null ? item.title : '支撐'; } else { price = item; lw = 1; ls = LW.LineStyle.Dashed; title = '支撐'; } priceLines.push(active.createPriceLine({ price: price, color: '#2a7a50', // hardcoded per handoff §3.3 / SKILL.md — LW Charts JS 內 CSS variable 無效 — CLAUDE.md §6 邊緣案例 PASS lineWidth: lw, lineStyle: ls, axisLabelVisible: true, title: title, })); }); }, setResistance: function (levels) { // 不清除(清除責任已移至 clearSR,避免把 setSupport 剛 push 的支撐線刪掉) (levels || []).forEach(function (item) { var price, lw, ls, title; if (item !== null && typeof item === 'object') { price = item.price; var scale = item.scale || ''; if (scale === 'long') { lw = 2; ls = LW.LineStyle.Solid; } else if (scale === 'mid') { lw = 1; ls = LW.LineStyle.Solid; } else { lw = 1; ls = LW.LineStyle.Dashed; } title = item.title != null ? item.title : '壓力'; } else { price = item; lw = 1; ls = LW.LineStyle.Dashed; title = '壓力'; } priceLines.push(active.createPriceLine({ price: price, color: '#c0392b', // hardcoded per handoff §3.3 / SKILL.md — LW Charts JS 內 CSS variable 無效 — CLAUDE.md §6 邊緣案例 PASS lineWidth: lw, lineStyle: ls, axisLabelVisible: true, title: title, })); }); }, destroy: function () { chart.remove(); }, }; };

本平台使用 Cookie 維持登入狀態,並在您同意後啟用分析功能以改善服務。 詳見 Cookie 政策隱私權政策

選擇要啟用的 Cookie 類型