| name | finlab |
| description | Comprehensive guide for FinLab quantitative trading package across global stock markets (TW, US, KR, JP, HK; both single-name equities and ETFs/funds). Use when working with trading strategies, backtesting, stock data, FinLabDataFrame, factor analysis, stock selection, or when the user mentions FinLab, trading, quant trading, US equity, S&P 500 / NASDAQ 100, SPY / QQQ, sector or leveraged ETFs, ETF rotation, 美股, or stock market analysis. Includes data access, strategy development, backtesting workflows, best practices, and US-market specifics (data availability map, filing-date-aligned quarterly fundamentals, US universe construction, USMarket vs. USFundMarket defaults, and ETF backtesting). |
| compatibility | Requires Python 3.10+ and uv package manager (https://docs.astral.sh/uv/) |
FinLab Quantitative Trading Package
Prerequisites
Before running any FinLab code, verify these in order:
-
uv is installed (Python package manager):
uv --version
If uv is not installed, tell the user to install it.
After installing, ensure uv is on PATH:
source $HOME/.local/bin/env 2>/dev/null
-
FinLab is installed via uv (requires >= 2.0.0):
uv python install 3.12
uv pip install --system "finlab>=2.0.0" 2>/dev/null || uv pip install "finlab>=2.0.0"
Or use uv run for zero-setup execution (recommended for one-off scripts):
uv run --with "finlab" python3 script.py
uv run --with auto-creates a temporary environment with dependencies — no venv management needed.
-
API Token is set (required - finlab will fail without it):
If no token, use finlab's built-in login (available in >= 1.5.9, improved Firebase flow in v1.5.11):
import finlab
finlab.login()
This handles the full OAuth flow (browser login, token retrieval, .env storage) automatically.
Language
Respond in the user's language. If user writes in Chinese, respond in Chinese. If in English, respond in English.
Market Support
FinLab supports TW (default), US, KR, JP, and HK markets. The rest of this file plus dataframe-reference.md, backtesting-reference.md, best-practices.md, factor-analysis-reference.md, and machine-learning-reference.md are market-agnostic — the APIs behave the same across markets.
For US-market work — whether single-name equities (data.set_market('us')) or ETFs/funds (data.set_market('us_fund')) — read us-market.md first. Queries that should trigger it include: US equity, S&P 500, NASDAQ 100, 美股, SPY / QQQ, sector SPDRs, leveraged / inverse ETFs, ETF rotation, us_price:*, us_fund_price:*, data.us_universe(...), or us_income_statement:* / us_cash_flow:* / us_balance_sheet:*. It documents:
- Which US data tables are safe for backtesting versus current-snapshot-only (analyst consensus, ratios, DCF are live-only — do not use them historically)
- Filing-date-aligned quarterly fundamentals (
key_date == filing_date) — no .shift() workaround needed
Report API names on US (creturn / daily_creturn / get_stats(); no get_equity())
- US backtest defaults for both markets:
USMarket (fee_ratio=0, tax_ratio=0, trade_at_price='close') and USFundMarket for ETF/fund backtests
- How
data.set_market(...) is the session-scope switch (there is no market= kwarg on data.get())
- Dollar-volume-top-N universe construction (works back to 2016), S&P 500 / NASDAQ 100 membership via
data.us_universe(index='S&P 500' | 'NASDAQ 100') with its 2022-11 history-start caveat, quality gates, and sector-exclusion rationale
- Lookahead-bias checklist specific to US data (rolling-window universe filters, survivorship avoidance)
- ETF / sector-rotation backtesting via
USFundMarket and us_fund_price:*
Other-market queries can skip that file.
API Token Tiers & Usage
Token Tiers
| Tier | Daily Limit | Token Pattern |
|---|
| Free | 500 MB | ends with #free |
| VIP | 5000 MB | no suffix |
Usage Reset
- Resets daily at 8:00 AM UTC+8
- When limit exceeded, user must wait for reset or upgrade to VIP
Quick Start Example
from finlab import data
from finlab.backtest import sim
close = data.get("price:收盤價")
vol = data.get("price:成交股數")
pb = data.get("price_earning_ratio:股價淨值比")
cond1 = close.rise(10)
cond2 = vol.average(20) > 1000*1000
cond3 = pb.rank(axis=1, pct=True) < 0.3
position = cond1 & cond2 & cond3
position = pb[position].is_smallest(10)
report = sim(position, resample="M", upload=False)
print(report.metrics.annual_return())
print(report.metrics.sharpe_ratio())
print(report.metrics.max_drawdown())
stats = report.get_stats()
print(f"CAGR: {stats['cagr']:.2%}")
print(f"Sharpe: {stats['monthly_sharpe']:.2f}")
print(f"MDD: {stats['max_drawdown']:.2%}")
report
Core Workflow: 5-Step Strategy Development
Step 1: Fetch Data
Use data.get("<TABLE>:<COLUMN>") to retrieve data:
from finlab import data
close = data.get("price:收盤價")
volume = data.get("price:成交股數")
roe = data.get("fundamental_features:ROE稅後")
revenue = data.get("monthly_revenue:當月營收")
pe = data.get("price_earning_ratio:本益比")
pb = data.get("price_earning_ratio:股價淨值比")
foreign_buy = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)")
rsi = data.indicator("RSI", timeperiod=14)
macd, macd_signal, macd_hist = data.indicator("MACD", fastperiod=12, slowperiod=26, signalperiod=9)
Filter by market/category using data.universe():
with data.universe(market='TSE_OTC', category=['水泥工業']):
price = data.get('price:收盤價')
data.set_universe(market='TSE_OTC', category='半導體')
Use data.search('keyword', market='<market>') to discover available datasets. Supported markets: tw, us, kr, jp, hk. Use keywords in the dataset's native language (e.g. data.search('營收', market='tw'), data.search('revenue', market='us')).
Step 2: Create Factors & Conditions
Use FinLabDataFrame methods to create boolean conditions:
rising = close.rise(10)
sustained_rise = rising.sustain(3)
sma60 = close.average(60)
above_sma = close > sma60
top_market_value = data.get('etl:market_value').is_largest(50)
low_pe = pe.rank(axis=1, pct=True) < 0.2
industry_top = roe.industry_rank() > 0.8
See dataframe-reference.md for all FinLabDataFrame methods.
Step 3: Construct Position DataFrame
Combine conditions with & (AND), | (OR), ~ (NOT):
position = cond1 & cond2 & cond3
position = factor[condition].is_smallest(10)
entries = close > close.average(20)
exits = close < close.average(60)
position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
Important: Position DataFrame should have:
- Index: DatetimeIndex (dates)
- Columns: Stock IDs (e.g., '2330', '1101')
- Values: Boolean (True = hold) or numeric (position size)
Step 4: Backtest
from finlab.backtest import sim
report = sim(position, resample="M")
report = sim(
position,
resample="M",
stop_loss=0.08,
take_profit=0.15,
trail_stop=0.05,
position_limit=1/3,
fee_ratio=1.425/1000/3,
tax_ratio=3/1000,
trade_at_price='open',
upload=False
)
print(f"Annual Return: {report.metrics.annual_return():.2%}")
print(f"Sharpe Ratio: {report.metrics.sharpe_ratio():.2f}")
print(f"Max Drawdown: {report.metrics.max_drawdown():.2%}")
stats = report.get_stats()
print(f"CAGR: {stats['cagr']:.2%}")
print(f"Sharpe: {stats['monthly_sharpe']:.2f}")
print(f"MDD: {stats['max_drawdown']:.2%}")
See backtesting-reference.md for complete sim() API.
Step 5: Execute Orders (Optional)
Convert backtest results to live trading:
from finlab.online.order_executor import Position, OrderExecutor
from finlab.online.sinopac_account import SinopacAccount
position = Position.from_report(report, fund=1000000)
acc = SinopacAccount()
executor = OrderExecutor(position, account=acc)
executor.create_orders(view_only=True)
executor.create_orders()
See trading-reference.md for complete broker setup and OrderExecutor API.
Reference Files
What's New (since v1.5.8)
Short version pointers for features added in recent releases. Each reference file tags the exact API with (vX.Y.Z).
v2.0.0 (2026-04-04) — major release
finlab.exceptions: structured error hierarchy (FinlabError, DataError, BacktestError, ...) — see backtesting-reference.md
data.get(lazy=True) / data.gets(..., lazy=True): batch fetch + deferred compute; data.override() / DataContext for scoped global state
df.cs / df.sector / df.weight accessors; rolling().std/var/skew/kurt/median — see dataframe-reference.md
PositionStreamMixin for realtime position streaming — see trading-reference.md
from finlab import FinlabDataFrame top-level export
backtest.sim() refactored into 5 testable stages; eval() removed from optimize.combinations
v1.5.13 (2026-03-22)
universe(index=...) / us_universe(index=...): filter US stocks by S&P 500 / NASDAQ 100
- New market code
TW_CB (TW convertible bonds)
v1.5.11 (2026-03-11)
data.get_role() / data.is_vip(): query user quota tier
- Report migration to canonical Firestore flow (transparent to users)
v1.5.9
finlab.schemas: typed PositionEntry, OrderEntry, PortfolioData contracts
OrderExecutor.generate_orders(as_entries, quantity_type) and generate_order_entries()
PortfolioSyncManager.get_data_typed() / set_data_typed()
data.get() 80% quota usage warning
sim() uses market-specific default fee_ratio / tax_ratio (no longer hardcoded TW values)
v1.5.8 (baseline)
verify_strategy(): automated lookahead-bias detector
report.to_terminal(): ASCII report for non-Jupyter runs
- Overall strategy execution 3.4x faster
Prevent Lookahead Bias
Critical: Avoid using future data to make past decisions:
prev_close = close.shift(1)
See best-practices.md for more anti-patterns.
Performance Defaults
Pass lazy=True by default; drop to eager pandas only when debugging. data.get(..., lazy=True) and data.gets(..., lazy=True) (v2.0.0) return lazy FinlabDataFrames that defer the compute graph until a terminal call materializes it — chained ops avoid redundant passes (single-CPU). Omit lazy=True when you need to print/inspect intermediate values interactively.
price, volume, pe = data.gets(
'price:收盤價', 'price:成交股數', 'price_earning_ratio:本益比',
lazy=True,
)
close = data.get('price:收盤價')
print(close.loc['2024-01-15', '2330'])
Feedback
Direct users to open an issue on GitHub: https://github.com/koreal6803/finlab-ai/issues
Notes
- Some data columns use Chinese names — this is expected, use them as-is in
data.get() calls
- Data frequency varies: daily (price), monthly (revenue), quarterly (financial statements)
- Always use
sim(..., upload=False) for experiments, upload=True only for final production strategies