2026/4/12

Kronos 技術深度分析:金融K線首個基礎模型

1. 為何需要專門的金融K線 Foundation Model?

時間序列基礎模型(TSFM)在電力、醫療等領域已有成功案例,但金融 K線資料面臨獨特挑戰:

  • K線具有多維結構(OHLCVA),難以直接套用一般 Transformer
  • 市場具有高噪聲、非穩態特性,過去規律不一定適用未來
  • 不同交易所的交易規則、結算制度、漲跌限制各異,跨市場泛化困難

Kronos(arXiv:2508.02739,AAAI 2026)針對這些問題提出系統性解決方案,是第一個開源的金融 K線基礎模型。

2. 技術架構:兩階段框架

Stage 1:K-line Tokenizer(BSQ)

使用 Binary Spherical Quantization 將連續多維的 OHLCVA 向量量化為離散 tokens。這個 tokenizer 並非一般 NLP 的 BPE,而是針對金融市場價格動態專門設計,能保留價格區間、相對變化等關鍵資訊。

Stage 2:Autoregressive Transformer

Decoder-only 架構,在量化後的 token 序列上做自回歸預測。預訓練目標為「預測下一個 K-line token」,與 GPT 系列語言模型的訓練邏輯一致。

3. 模型版本與選擇

模型TokenizerContext Length參數量開源
Kronos-miniKronos-Tokenizer-2k20484.1M
Kronos-smallKronos-Tokenizer-base51224.7M
Kronos-baseKronos-Tokenizer-base512102.3M
Kronos-largeKronos-Tokenizer-base512499.2M

預訓練規模:120 億筆 K線記錄、45 個交易所、7 種時間粒度。
HuggingFace:NeoQuasar/Kronos-small

4. 台灣股市應用評估

優勢

  • 預訓練涵蓋45個交易所、120億筆 K線,台灣屬亞洲新興市場,已有跨市場泛化能力
  • Zero-shot RankIC 領先其他 TSFM 93%
  • Kronos-mini(4.1M)可在一般 GPU 執行,適合個人或小型團隊研究
  • 不需自行設計特徵工程或時間序列標記

限制

  • Context Length 限制(512 ~ 2048),日K約1.4年至8年
  • 不看財報、總經、籌碼等非價格資訊
  • 台灣特有的政策干預、權值股主導特性未被訓練進去
  • ±10% 漲跌限制、T+2 結算等台灣制度細節,建議透過 Fine-tuning 針對優化

5. 建議流程

  1. 回測優先:以歷史日K做滾動窗口回測,觀察模型在多頭、空頭、震盪不同市場型態的表現
  2. 參考輔助:Kronos 預測結果作為技術面輔助參考,不作唯一進出场依據
  3. 長期評估 Fine-tuning:如欲針對台股特性微調,需較大 GPU 資源與乾淨歷史資料

參考連結

Kronos GitHubgithub.com/shiyu-coder/Kronos
Kronos 論文arxiv.org/abs/2508.02739
HuggingFace 模型NeoQuasar/Kronos-small
twstock 套件github.com/mlouielu/twstock

研究時間:2026-04-12

Kronos 安裝腳本與台灣股市預測實作

1. 安裝腳本

#!/bin/bash
# Kronos 安裝腳本(Python 需 3.10+)
set -e

KRONOS_DIR="/home/charles-chang/.openclaw/workspace/research/2026-04-12_kronos_taiwan_stock/kronos_repo"
ENV_DIR="/home/charles-chang/kronos_env"

# 建立虛擬環境
if [ ! -d "$ENV_DIR" ]; then
    python3 -m venv $ENV_DIR
fi
source $ENV_DIR/bin/activate

# 安裝依賴
pip install --upgrade pip -q
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 -q
pip install yfinance pandas transformers einops -q

# 克隆 Kronos(如尚未存在)
if [ ! -d "$KRONOS_DIR" ]; then
    git clone https://github.com/shiyu-coder/Kronos.git $KRONOS_DIR
fi

# 驗證模型
python3 <<'PYEOF'
import sys
sys.path.insert(0, '/home/charles-chang/.openclaw/workspace/research/2026-04-12_kronos_taiwan_stock/kronos_repo')
from model import Kronos, KronosTokenizer, KronosPredictor
tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base")
model = Kronos.from_pretrained("NeoQuasar/Kronos-small")
predictor = KronosPredictor(model, tokenizer, max_context=512)
print("模型載入成功 ✓")
PYEOF

2. 台灣股市預測腳本

#!/usr/bin/env python3
"""
Kronos 台灣股市預測腳本
使用:python3 predict_taiwan_stock.py <股票代號> [--lookback N] [--pred_len N]
範例:python3 predict_taiwan_stock.py 2330 --pred_len 30
"""

import os, sys, argparse
from datetime import datetime, timedelta
import pandas as pd
import yfinance as yf

kronos_path = os.path.join(os.path.dirname(__file__), 'kronos_repo')
if os.path.exists(kronos_path):
    sys.path.insert(0, kronos_path)
from model import Kronos, KronosTokenizer, KronosPredictor


def fetch_taiwan_stock_data(stock_code: str, days: int = 800) -> pd.DataFrame:
    """從 Yahoo Finance 取得台股歷史 K線"""
    ticker = f"{stock_code}.TW"
    df = yf.download(ticker, period=f"{days}d", auto_adjust=False, progress=False)
    df = df.reset_index()
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = [col[0] if isinstance(col, tuple) else col for col in df.columns]
    rename_map = {}
    for col in df.columns:
        cl = str(col).lower()
        if 'date' in cl: rename_map[col] = 'timestamps'
        elif 'open' in cl and 'open' not in rename_map.values(): rename_map[col] = 'open'
        elif 'high' in cl and 'high' not in rename_map.values(): rename_map[col] = 'high'
        elif 'low' in cl and 'low' not in rename_map.values(): rename_map[col] = 'low'
        elif 'close' in cl: rename_map[col] = 'close'
        elif 'volume' in cl and 'volume' not in rename_map.values(): rename_map[col] = 'volume'
    df = df.rename(columns=rename_map)
    df = df.loc[:, ~df.columns.duplicated()]
    df['amount'] = 0.0
    for col in ['open', 'high', 'low', 'close', 'volume', 'amount']:
        df[col] = pd.to_numeric(df[col], errors='coerce')
    df = df.dropna()
    return df[['timestamps', 'open', 'high', 'low', 'close', 'volume', 'amount']]


def predict(df: pd.DataFrame, lookback: int = 400,
             pred_len: int = 30, sample_count: int = 1) -> pd.DataFrame:
    """Kronos-small K線預測"""
    tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base")
    model = Kronos.from_pretrained("NeoQuasar/Kronos-small")
    predictor = KronosPredictor(model, tokenizer, max_context=512)
    lookback = min(lookback, 512)
    df_in = df.tail(lookback).reset_index(drop=True)
    x_ts = df_in['timestamps'].reset_index(drop=True)
    y_ts = pd.Series(pd.date_range(
        start=df_in['timestamps'].iloc[-1] + timedelta(days=1),
        periods=pred_len, freq='B'))
    return predictor.predict(
        df=df_in[['open', 'high', 'low', 'close', 'volume', 'amount']],
        x_timestamp=x_ts, y_timestamp=y_ts,
        pred_len=pred_len, T=1.0, top_p=0.9, sample_count=sample_count)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('stock_code', help='股票代號(例:2330)')
    ap.add_argument('--lookback', type=int, default=400)
    ap.add_argument('--pred_len', type=int, default=30)
    ap.add_argument('--sample_count', type=int, default=1)
    args = ap.parse_args()
    df = fetch_taiwan_stock_data(args.stock_code, days=int(args.lookback * 2))
    pred = predict(df, args.lookback, args.pred_len, args.sample_count)
    print(f"\n{'日期':<12} {'Open':>10} {'High':>10} {'Low':>10} {'Close':>10}")
    print("-" * 60)
    for idx, row in pred.iterrows():
        ds = idx.strftime('%Y-%m-%d') if hasattr(idx,'strftime') else str(idx)
        f = lambda k: f"{row.get(k,0):>10.2f}" if isinstance(row.get(k,0),(int,float)) else f"{str(row.get(k,'')):>10}"
        print(f"{ds:<12} {f('open')} {f('high')} {f('low')} {f('close')}")
    out = f"prediction_{args.stock_code}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
    pred.to_csv(out, index_label='timestamps')
    print(f"\n已儲存:{out}\n此為模型輸出結果,僅供參考,不構成投資建議。")


if __name__ == "__main__":
    main()

3. 台灣股市資料來源

Yahoo Finance(yfinance)日K為主,即時性普通,3行代碼即完成
twstock 套件台灣原生,直接串接證交所與櫃買中心
Fugle API日內分鐘線最可靠
證交所開放資料 API官方 REST,即時性高
# Yahoo Finance 最簡範例
import yfinance as yf
df = yf.download("2330.TW", start="2020-01-01")  # 台積電

4. 使用方式

# 安裝環境
bash setup_kronos.sh

# 預測台積電未來30天
/home/charles-chang/kronos_env/bin/python3 predict_taiwan_stock.py 2330

# 指定歷史窗口與預測長度
/home/charles-chang/kronos_env/bin/python3 predict_taiwan_stock.py 2330 --lookback 200 --pred_len 10

5. 台股特殊考量

  • 漲跌停 ±10%:Fine-tuning 時不應過濾這些bars,是正常市場現象
  • 成交金額(amount):Yahoo Finance 無此欄位,以0填補即可
  • Context Length:Kronos-small 512 tokens(約1.4年日K);日內分鐘線建議用 Kronos-mini(2048)
  • 日內資料:Yahoo Finance 分鐘線在台股不可靠,建議改用 Fugle API

實作完成時間:2026-04-12

如何突破 Cloudflare 取得 Perplexity 內容

問題背景

在自動化研究場景中,常常需要透過 AI 搜尋引擎(如 Perplexity)抓取整理過的答案,並進一步餵給其他系統處理。然而 Perplexity 的分享連結受到 Cloudflare JS Challenge 保護,大多數程式化的存取方式都會被阻擋。

本文記錄完整的原因分析、所有嘗試過的失敗方法,以及最終如何透過 CloakBrowser 成功突破。

為什麼無法直接取得 Perplexity 內容?

根本原因:瀏覽器指紋偵測

Perplexity 使用的保護機制不是傳統的「封 IP」或「輸入驗證碼」,而是瀏覽器指紋偵測(Browser Fingerprinting)。

當請求抵達 Cloudflare 時,會檢測以下項目:

  • navigator.webdriver 是否為 true(自動化框架標記)
  • navigator.plugins.length 是否符合真實瀏覽器
  • window.chrome 物件是否存在
  • WebGL / Canvas / Audio 指紋是否正常
  • WebRTC 是否洩漏真實 IP
  • TLS 指紋(JA3/JA4)是否為常見瀏覽器

只要有任何一項不符,Cloudflare 就判定為機器人,發回 JS Challenge 頁面。

常見工具為何全部失效?

嘗試方法失敗原因
curl + 多種 User-Agent無法執行 JavaScript,只能拿到 Challenge 頁 HTML
web_fetch / cloudscraper403,cloudscraper 對新版 Cloudflare 失效
Playwright(標準版)navigator.webdriver=true,立即被偵測
Playwright + stealth 參數仍有殘留信號,觸發指紋檢查
undetected-chromedriver連線錯誤,新版 Chrome 不相容
curl-impersonateTLS 指紋可能僥倖通過,但 JS Challenge 仍需瀏覽器執行
Xvfb + patchright底層同樣缺乏完整指紋修補

這些方法的共同盲點是:都在試圖「欺騙」Cloudflare 的 JS 檢測層,而非真正讓檢測看到的都是正常值。

解決方案:CloakBrowser

工具介紹

CloakBrowser(GitHub: CloakHQ/CloakBrowser)是 目前唯一在 C++ 層級修改 Chromium 原始碼的指紋修補方案。不是設定調整,不是 JS 注入,而是49 個指紋 patch 直接編進二進位,包括:

  • navigator.webdriver → false
  • navigator.plugins.length → 真實 plugin 清單
  • window.chrome → 正常瀏覽器物件
  • WebGL / Canvas / Audio 指紋
  • Font / Screen / Hardware 指紋
  • WebRTC IP leak 防護
  • TLS 指紋(JA3/JA4/Akamai)
  • Automation signals(CDP detection)

安裝方式

pip install cloakbrowser

Python 使用範例

from cloakbrowser import launch
import time

browser = launch(headless=False)
page = browser.new_page()

page.goto(
    "https://www.perplexity.ai/search/你的分享連結",
    wait_until="domcontentloaded"
)

# 等待 Cloudflare 挑戰自動通過
page.wait_for_function(
    "() => !document.title.includes('Just a moment')",
    timeout=40000
)
time.sleep(3)  # 額外等待確保渲染完成

html = page.content()
browser.close()

print(f"成功取得內容,HTML 長度: {len(html)} bytes")

關鍵實務細節

  1. 使用 headful 模式(headless=False):相較 headless,headful 的指紋更難被偵測,搭配 Xvfb 虛擬顯示器即可在無頭伺服器上運行
  2. 等待 Challenge 完成:wait_for_function 監控標題是否離開「請稍候」頁,而非盲目 sleep
  3. 額外等待 3 秒:確保 Perplexity 的 SPA 完全渲染動態內容

失敗方法完整列表

方法結果瓶頸
curl + UA 輪換無法執行 JS
cloudscraperCloudflare 版本過新
Playwright 標準版webdriver flag
Playwright + stealth仍有殘留信號
undetected-chromedriverChrome 版本不相容
curl-impersonateJS Challenge 仍需瀏覽器
Xvfb + patchright指紋修補不完整
CloakBrowser✅ 成功C++ 層級完整修補

結論

突破 Cloudflare JS Challenge 的核心瓶頸不在於「如何繞過檢測」,而在於讓檢測看到的全部都是正常瀏覽器值。CloakBrowser 的價值在於它從 Chromium 原始碼層面解決了這個問題,而不是停留在 JS 層或參數層的半成品繞過。

對於需要自動化存取受保護網頁的系統,CloakBrowser 是目前已知最穩定、成功率最高的方案。

(本文同步發布於研究記錄 2026-04-12)