finlab — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited finlab (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Before running any FinLab code, verify these in order:
uv --versionIf 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 # Add uv to current shell uv python install 3.12 # Ensure Python is available (skip if already installed)
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.pyuv run --with auto-creates a temporary environment with dependencies — no venv management needed.
Prefer zero-install? Run notebooks directly in FinLab Studio — a hosted Jupyter environment with finlab preinstalled and your account already logged in.
Desktop (has a browser): log in once; credentials are cached locally:
python -m finlab login import finlab
finlab.login() # reuses cached credentials, or starts browser login if noneHeadless / cron / Docker: on a machine with a browser, log in and run:
python -m finlab token --envSet all three exported variables — FINLAB_REFRESH_TOKEN, FINLAB_SESSION_ID, and FINLAB_API_KEY — in the headless environment. FinLab uses them automatically. Treat the output as credentials: use your platform's secret storage and do not paste it into chat or commit it.
Google Colab: run finlab.login() in a cell and follow the login link.
Migrating from legacy tokens: FINLAB_API_TOKEN and finlab.login('<api_token>') are deprecated. Run python -m finlab migrate, set up browser login or the three environment variables above, then remove the old token from your configuration.
Legacy-token status: No removal version or date is publicly confirmed in the official authentication guide; the 2026/08/01 date in client warning text is not a removal announcement. Dropping the client-side fallback and rejecting legacy tokens server-side are separate changes, and a cached data.get() call does not test server authentication — use the supported login flow above.
Respond in the user's language. If user writes in Chinese, respond in Chinese. If in English, respond in English.
FinLab supports TW (default), US, KR, JP, HK, plus Taiwan emerging (rotc) and Taiwan convertible bonds (tw_cb). Pick the market once per session with data.set_market(<code>); an unprefixed dataset name resolves to the active market's table (price:close → us_price:close under us). A name the active market lacks silently falls back to the TW table: Chinese keys (price:收盤價, monthly_revenue:當月營收) exist only for TW and rotc, so under us, data.get('price:收盤價') returns Taiwan prices. Outside TW, use English keys (price:close) or prefixed names (us_price:adj_close). data.set_market('rotc') (v2.0.9) enables 興櫃 (TW emerging) — use it when you need pre-listing price action or revenue factors that don't exist in the main TSE/OTC catalog. tw_cb is not a set_market code (it raises ValueError): read cb_price:* and pass sim(position, market=TWCBMarket()) (from finlab.markets.tw_cb import TWCBMarket).
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](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:
key_date — no .shift() workaround needed; the raw filing_date column is not the publication dateReport API names on US (creturn / daily_creturn / get_stats(); no get_equity())USMarket (fee_ratio=0, tax_ratio=0, trade_at_price='close') and USFundMarket for ETF/fund backtestsdata.set_market(...) is the session-scope switch (there is no market= kwarg on data.get())us_index_constituents:* (data.us_universe(index=...) is not point-in-time), quality gates, and sector-exclusion rationaleUSFundMarket and us_fund_price:*Other-market queries can skip that file.
| Tier | Daily Limit |
|---|---|
| Free | 500 MB |
| VIP | 5000 MB |
Check the current plan and quota with python -m finlab status.
from finlab import data
from finlab.backtest import sim
# 1. Fetch data
close = data.get("price:收盤價")
vol = data.get("price:成交股數")
pb = data.get("price_earning_ratio:股價淨值比")
# 2. Create conditions
cond1 = close.rise(10) # Rising last 10 days
cond2 = vol.average(20) > 1000*1000 # High liquidity
cond3 = pb.rank(axis=1, pct=True) < 0.3 # Low P/B ratio
# 3. Combine conditions and select stocks
position = cond1 & cond2 & cond3
position = pb[position].is_smallest(10) # Top 10 lowest P/B
# 4. Backtest
report = sim(position, resample="M", upload=False)
# 5. Print metrics - Two equivalent ways:
# Option A: Using metrics object
print(report.metrics.annual_return())
print(report.metrics.sharpe_ratio())
print(report.metrics.max_drawdown())
# Option B: Using get_stats() dictionary (different key names!)
stats = report.get_stats()
print(f"CAGR: {stats['cagr']:.2%}")
print(f"Sharpe: {stats['daily_sharpe']:.2f}")
print(f"MDD: {stats['max_drawdown']:.2%}")
# Benchmark stats (finlab >= 2.0.17): same keys via report.get_benchmark_stats()
# 6. Write the FinLab-generated HTML report (REQUIRED — do not hand-roll your own HTML)
report.to_html("report.html")
print("Open report.html to inspect equity curve, monthly returns, drawdown, and trade list.")Use data.get("<TABLE>:<COLUMN>") to retrieve data:
from finlab import data
# Price data
close = data.get("price:收盤價") # TWD
volume = data.get("price:成交股數") # shares
# Financial statements
roe = data.get("fundamental_features:ROE稅後") # %
revenue = data.get("monthly_revenue:當月營收") # thousand TWD, dated when disclosed: the 2026-10-12 row is September
eps = data.get("financial_statement:每股盈餘") # TWD, that quarter alone
# Valuation
pe = data.get("price_earning_ratio:本益比")
pb = data.get("price_earning_ratio:股價淨值比")
# Institutional trading
foreign_buy = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)") # shares
# Market benchmark
taiex = data.get("taiex_total_index:收盤指數") # TAIEX close
# Technical indicators
rsi = data.indicator("RSI", timeperiod=14)
macd, macd_signal, macd_hist = data.indicator("MACD", fastperiod=12, slowperiod=26, signalperiod=9)Taiwan tables keep the exchange's units. Money in monthly_revenue:* and financial_statement:* is thousand TWD (千元): a monthly revenue of 214,752,761 is NT$214.75 billion. Prices and per-share figures are TWD; volumes and institutional net buys are shares. Income-statement fields of financial_statement:*, EPS included, cover that quarter alone, not the year to date. Monthly revenue is dated when it is disclosed, by the 10th of the following month: the row dated 2026-10-12 is September's revenue, so name the month it covers, not the month of its date. data.describe("<TABLE>:<COLUMN>") returns a field's catalog description.
Filter by market/category using `data.universe()`:
# Limit to specific industry
with data.universe(market='TSE_OTC', category=['水泥工業']):
price = data.get('price:收盤價')
# Set globally
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')).
Use FinLabDataFrame methods to create boolean conditions:
# Trend
rising = close.rise(10) # Rising vs 10 days ago
sustained_rise = rising.sustain(3) # Rising for 3 consecutive days
# Moving averages
sma60 = close.average(60)
above_sma = close > sma60
# Ranking
top_market_value = data.get('etl:market_value').is_largest(50)
low_pe = pe.rank(axis=1, pct=True) < 0.2 # Bottom 20% by P/E
# Industry ranking
industry_top = roe.industry_rank() > 0.8 # Top 20% within industrySee dataframe-reference.md for all FinLabDataFrame methods.
Combine conditions with & (AND), | (OR), ~ (NOT):
# Simple position: hold stocks meeting all conditions
position = cond1 & cond2 & cond3
# Limit number of stocks
position = factor[condition].is_smallest(10) # Hold top 10
# Entry/exit signals with hold_until
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:
from finlab.backtest import sim
# Basic backtest
report = sim(position, resample="M")
# With risk management
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
)
# Extract metrics - Two ways:
# Option A: Using metrics object
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%}")
# Option B: Using get_stats() dictionary (note: different key names!)
stats = report.get_stats()
print(f"CAGR: {stats['cagr']:.2%}") # 'cagr' not 'annual_return'
print(f"Sharpe: {stats['daily_sharpe']:.2f}") # 'daily_sharpe' = metrics.sharpe_ratio(), not 'monthly_sharpe'
print(f"MDD: {stats['max_drawdown']:.2%}") # same name
# Benchmark comparison (finlab >= 2.0.17): same ffn keys, computed on the
# market benchmark over the backtest period — no need to recompute from
# market.get_benchmark()
bench = report.get_benchmark_stats()
print(f"Benchmark CAGR: {bench['cagr']:.2%} | MDD: {bench['max_drawdown']:.2%}")See backtesting-reference.md for complete sim() API.
Follow each backtest the user will review with one HTML file — the one FinLab generates:
report = sim(position, resample="M", upload=False)
report.to_html("report.html") # the FinLab-generated file is the deliverableThe canonical deliverable is the file generated by report.to_html() — do not hand-roll a separate report (custom HTML pages, Plotly summaries, dashboards, markdown files) unless the user explicitly asks. To summarize results, print a short terminal summary and point to the FinLab report. Exception: in batch runs (parameter sweeps, screening many variants), skip per-run HTML and write it only for the final strategy the user will review.
Pick a descriptive filename when running more than one strategy in the same session (e.g. momentum_top10.html, value_lowpb.html) so the user can compare without overwriting. After writing, tell the user the path so they can open it. Use report.to_terminal() only as a supplement for non-GUI terminals; it does not replace the HTML.
See the "report.to_html() — the canonical deliverable" section of backtesting-reference.md for details on what the file contains.
Convert backtest results to live trading:
from finlab.online.order_executor import Position, OrderExecutor
from finlab.online.sinopac_account import SinopacAccount
# 1. Convert report to position
position = Position.from_report(report, fund=1000000)
# 2. Connect broker account
acc = SinopacAccount()
# 3. Create executor and preview orders
executor = OrderExecutor(position, account=acc)
executor.create_orders(view_only=True) # Preview first
# 4. Execute orders (when ready)
executor.create_orders()See trading-reference.md for complete broker setup and OrderExecutor API.
| File | Content |
|---|---|
| backtesting-reference.md | sim() 參數、stop-loss、rebalancing |
| trading-reference.md | 券商設定、OrderExecutor、Position |
| factor-examples.md | 60+ 策略範例 |
| dataframe-reference.md | FinLabDataFrame 方法 |
| factor-analysis-reference.md | IC、Shapley、因子分析 |
| best-practices.md | 常見錯誤、lookahead bias |
| machine-learning-reference.md | ML 特徵工程 |
| us-market.md | US market specifics: data map, quarterly alignment, defaults, universe construction |
Short version pointers for features added in recent releases. Each reference file tags the exact API with (vX.Y.Z).
v2.0.15 (2026-07-18)
df.sector(by=...): sector accessor now accepts a custom classification — dict / pd.Series (stock_id → group) or a time-varying pd.DataFrame; unlisted stocks are excluded. Works with all sector.* methods — see dataframe-reference.mddf.sector.map(mapping): broadcast group-level scalars (e.g. sector weights) to full DataFrame shape for factor composition — see dataframe-reference.mddf.weight.by_group(weights, by, default): allocate capital across sectors/groups — normalize holdings so each group's total equals its share; under-allocation stays in cash — see dataframe-reference.mdv2.0.12 (2026-06-01)
sim() / hold_until(): trail_stop_activation — require a minimum unrealized gain before trail_stop arms. See backtesting-reference.md and dataframe-reference.mdreport.to_html(path, title=...): standalone HTML now sets browser-tab title + FinLab favicon; pass title to disambiguate multi-strategy report folders — see backtesting-reference.mdv2.0.9 (2026-05-27)
data.set_market("rotc"): 興櫃 is now a first-class market code; price:收盤價 / monthly_revenue:* / etc. resolve to the rotc_ catalog and sim() uses ROTCMarket defaultsdata.search(market="rotc"): scoped to the emerging-market catalog onlyv2.0.1 (2026-04-26)
python -m finlab cloud (CLI): deploy strategies to the finlab-auto-update Cloud Functions runtime with daily Asia/Taipei scheduling — deploy, get, list, run, logs, schedule set/delete, delete, status. See trading-reference.mdsim() peak RSS ~800 MB lower on full-market monthly strategies (was ~2.0–2.2 GiB → ~1.29 GiB); enables s-tier cloud workers that previously OOM'dv2.0.0 (2026-04-04) — major release
finlab.exceptions: structured error hierarchy (FinlabError, DataError, BacktestError, ...) — see backtesting-reference.mddata.get(lazy=True) / data.gets(..., lazy=True): batch fetch + deferred compute; DataContext.override context manager for scoped statedf.cs / df.sector / df.weight accessors; rolling().std/var/skew/kurt/median — see dataframe-reference.mdPositionStreamMixin for realtime position streaming — see trading-reference.mdfrom finlab import FinlabDataFrame top-level exportbacktest.sim() refactored into 5 testable stages; eval() removed from optimize.combinationsv1.5.13 (2026-03-22)
universe(index=...) / us_universe(index=...): filter US stocks by S&P 500 / NASDAQ 100TW_CB (TW convertible bonds)v1.5.11 (2026-03-11)
data.get_role() / data.is_vip(): query user quota tierv1.5.9
finlab.schemas: typed PositionEntry, OrderEntry, PortfolioData contractsOrderExecutor.generate_orders(as_entries, quantity_type) and generate_order_entries()PortfolioSyncManager.get_data_typed() / set_data_typed()data.get() 80% quota usage warningsim() uses market-specific default fee_ratio / tax_ratio (no longer hardcoded TW values)v1.5.8 (baseline)
verify_strategy(): automated lookahead-bias detectorreport.to_terminal(): ASCII report for non-Jupyter runsNever use reindex() or reindex_like(), including on final positions. FinLabDataFrame automatically reshapes dates and stock columns during arithmetic, comparisons and boolean operations: sales_to_price = eps_sales / close. Do not manually align or bypass this behavior with pandas/NumPy. Calculate quarterly shifts and rolling windows before combining with daily data; set rebalance schedules with sim(position, resample=...). Use index_str_to_date() only to inspect dates. On errors, inspect types and consult dataframe-reference.md.
Critical: Avoid using future data to make past decisions:
# ✅ GOOD: Use shift(1) to get previous value
prev_close = close.shift(1)
# ❌ BAD: Don't use iloc[-2] (can cause lookahead)
# prev_close = close.iloc[-2] # WRONG
# ✅ GOOD: Leave index as-is even with strings like "2025Q1"
# FinLabDataFrame operators align dates and stock columns automatically
# ❌ BAD: Don't manually assign to df.index
# df.index = new_index # FORBIDDENSee best-practices.md for more anti-patterns.
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.
# ✅ Default: fetch lazy directly
price, volume, pe = data.gets(
'price:收盤價', 'price:成交股數', 'price_earning_ratio:本益比',
lazy=True,
)
# ✅ Debug: eager pandas for row-level inspection
close = data.get('price:收盤價')
print(close.loc['2024-01-15', '2330'])Direct users to open an issue on GitHub: https://github.com/koreal6803/finlab-ai/issues
data.get() callssim() defaults to upload=None (v2.1.0): it uploads only when FINLAB_FORCED_STRATEGY_NAME is set (cloud-scheduled runs, notebooks run on the FinLab Studio website); FINLAB_STRATEGY_NAME alone no longer triggers an upload. Local scripts, Jupyter and Colab do not upload unless you pass upload=True (or call report.upload() afterwards); upload=False never uploads~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.