
Finlab
- 1 installs
- 407 repo stars
- Updated July 19, 2026
- koreal6803/finlab-ai-plugin
This is a copy of finlab by koreal6803 - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
finlab is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- finlab
- AI & Agent Building
- AI-coding skill
Finlab by the numbers
- 1 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/koreal6803/finlab-ai-plugin --skill finlabAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 407 |
| Last updated | July 19, 2026 |
| Repository | koreal6803/finlab-ai-plugin ↗ |
What it does
Helps with ai & agent building tasks.
Files
FinLab Quantitative Trading Package
Prerequisites
Before running any FinLab code, verify these in order:
1. uv is installed (Python package manager):
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 shell2. FinLab is installed via uv (requires >= 2.0.0):
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.
3. 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() # Opens browser for Google OAuth, saves token automaticallyThis 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](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 ReportAPI names on US (creturn/daily_creturn/get_stats(); noget_equity())- US backtest defaults for both markets:
USMarket(fee_ratio=0,tax_ratio=0,trade_at_price='close') andUSFundMarketfor ETF/fund backtests - How
data.set_market(...)is the session-scope switch (there is nomarket=kwarg ondata.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
USFundMarketandus_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
# 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['monthly_sharpe']:.2f}")
print(f"MDD: {stats['max_drawdown']:.2%}")
reportCore Workflow: 5-Step Strategy Development
Step 1: Fetch Data
Use data.get("<TABLE>:<COLUMN>") to retrieve data:
from finlab import data
# Price data
close = data.get("price:收盤價")
volume = data.get("price:成交股數")
# Financial statements
roe = data.get("fundamental_features:ROE稅後")
revenue = data.get("monthly_revenue:當月營收")
# Valuation
pe = data.get("price_earning_ratio:本益比")
pb = data.get("price_earning_ratio:股價淨值比")
# Institutional trading
foreign_buy = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)")
# Technical indicators
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()`:
# 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')).
Step 2: Create Factors & Conditions
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.
Step 3: Construct Position DataFrame
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:
- 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
# 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['monthly_sharpe']:.2f}") # 'monthly_sharpe' not 'sharpe_ratio'
print(f"MDD: {stats['max_drawdown']:.2%}") # same nameSee 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
# 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.
Reference Files
| 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 |
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.mddata.get(lazy=True)/data.gets(..., lazy=True): batch fetch + deferred compute;data.override()/DataContextfor scoped global statedf.cs/df.sector/df.weightaccessors;rolling().std/var/skew/kurt/median— see dataframe-reference.mdPositionStreamMixinfor realtime position streaming — see trading-reference.mdfrom finlab import FinlabDataFrametop-level exportbacktest.sim()refactored into 5 testable stages;eval()removed fromoptimize.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: typedPositionEntry,OrderEntry,PortfolioDatacontractsOrderExecutor.generate_orders(as_entries, quantity_type)andgenerate_order_entries()PortfolioSyncManager.get_data_typed()/set_data_typed()data.get()80% quota usage warningsim()uses market-specific defaultfee_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 runs- Overall strategy execution 3.4x faster
Prevent Lookahead Bias
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 aligns by shape automatically
# ❌ BAD: Don't manually assign to df.index
# df.index = new_index # FORBIDDENSee 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.
# ✅ 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'])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=Trueonly for final production strategies
Backtesting Reference
Overview
The FinLab backtesting framework allows you to simulate trading strategies using historical data. The backtest.sim() function is the core tool for evaluating strategy performance, supporting various parameters for rebalancing, transaction costs, stop-loss/take-profit, and more.
---
backtest.sim
Simulate the equity curve of a stock portfolio based on its position history and market data. This function supports various parameters for rebalancing frequency, transaction costs, stop loss/take profit, and notification via Line.
Import:
from finlab import backtestSignature:
sim(
position: Union[pd.DataFrame, pd.Series],
resample: Union[str, None] = None,
resample_offset: Union[str, None] = None,
trade_at_price: Union[str, pd.DataFrame] = 'close',
position_limit: float = 1,
fee_ratio: float = 1.425/1000,
tax_ratio: float = 3/1000,
name: str = '未命名',
stop_loss: Union[float, None] = None,
take_profit: Union[float, None] = None,
trail_stop: Union[float, None] = None,
touched_exit: bool = False,
retain_cost_when_rebalance: bool = False,
stop_trading_next_period: bool = True,
live_performance_start: Union[str, None] = None,
mae_mfe_window: int = 0,
mae_mfe_window_step: int = 1,
market: Union[None, Market] = None,
upload: bool = True,
fast_mode: bool = False,
notification_enable: bool = False,
line_access_token: str = ''
) -> report.ReportParameters
position
- Type:
Union[pd.DataFrame, pd.Series] - Required: Yes
- Description: A pandas DataFrame or Series representing the buy/sell signals (True indicates holding, False indicates no position). For short positions, negative values can be used. The index should be a DatetimeIndex, and the columns should represent stock IDs.
resample
- Type:
Union[str, None] - Default:
None - Description: Trading frequency or rebalancing dates specification. It can be a string (e.g., 'D', 'W', 'M'), a DataFrame, Series, or None. When None, rebalancing only occurs on changes in the position.
resample_offset
- Type:
Union[str, None] - Default:
None - Description: An optional time offset (e.g., '1D', '1H') applied to rebalance dates.
trade_at_price
- Type:
Union[str, pd.DataFrame] - Default:
'close' - Description: Specifies which market price to use in the simulation. Options include 'close', 'open', 'open_close_avg', 'high_low_avg', or a custom DataFrame with price data.
position_limit
- Type:
float - Default:
1 - Description: Limit for the maximum weight assigned to any single asset (e.g., 0.2 for 20%).
fee_ratio
- Type:
float - Default: Market-specific (v1.5.9) — TW:
1.425/1000, US: resolved from the activeMarketsubclass - Description: Commission fee ratio applied during trades. When not explicitly provided,
sim()consults the targetMarketfor its default instead of hardcoding TW-market values, so US/CB backtests use the correct fee schedule automatically.
tax_ratio
- Type:
float - Default: Market-specific (v1.5.9) — TW:
3/1000, US:0 - Description: Transaction tax ratio applied when selling stocks. Resolved from the active
Marketwhen omitted.
name
- Type:
str - Default:
'未命名' - Description: Name for the strategy (for reporting purposes).
stop_loss
- Type:
Union[float, None] - Default:
None - Description: Stop loss percentage threshold. If set to None, stop loss is disabled.
take_profit
- Type:
Union[float, None] - Default:
None - Description: Take profit percentage threshold. If set to None, take profit is disabled.
trail_stop
- Type:
Union[float, None] - Default:
None - Description: Trailing stop threshold. If set to None, trailing stop is disabled.
touched_exit
- Type:
bool - Default:
False - Description: Flag to enable price touch exit logic. Use with caution as it may affect candle details.
retain_cost_when_rebalance
- Type:
bool - Default:
False - Description: Whether to carry forward the original cost basis when rebalancing positions.
stop_trading_next_period
- Type:
bool - Default:
True - Description: If a stop loss/take profit event occurs, trading is suspended for the next period.
mae_mfe_window
- Type:
int - Default:
0 - Description: Window length for calculating maximum adverse excursion (MAE) and maximum favorable excursion (MFE).
mae_mfe_window_step
- Type:
int - Default:
1 - Description: Step interval for the MAE/MFE analysis.
upload
- Type:
bool - Default:
True - Description: Determines whether to upload the strategy performance report after simulation.
Returns
An instance of Report containing performance metrics, trades, and additional analyses.
---
Report Class Reference
The sim() function returns a Report object with multiple APIs for accessing performance metrics.
Method 1: report.metrics (Recommended for single metrics)
Access individual metrics via the Metrics instance:
report = sim(position, resample="M", upload=False)
# Individual metric methods
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%}")Method 2: report.get_stats() (Returns dictionary)
Returns a flat dictionary with all stats. Useful for batch access:
stats = report.get_stats()
# Dictionary keys (note: different names than metrics methods!)
print(f"Annual Return: {stats['cagr']:.2%}")
print(f"Sharpe Ratio: {stats['monthly_sharpe']:.2f}")
print(f"Max Drawdown: {stats['max_drawdown']:.2%}")
print(f"Win Ratio: {stats['win_ratio']:.2%}")
print(f"Total Return: {stats['total_return']:.2%}")Available keys in `get_stats()`:
cagr- Compound Annual Growth Ratedaily_sharpe- Daily Sharpe ratiomonthly_sharpe- Monthly Sharpe ratiomax_drawdown- Maximum drawdown (negative value)win_ratio- Win rate of tradestotal_return- Total cumulative returnstart- Backtest start date (string)end- Backtest end date (string)return_table- Dict of monthly returns by year
Method 3: report.get_metrics() (Structured nested dictionary)
Returns a nested dictionary organized by category:
metrics = report.get_metrics()
# Structured access
print(metrics['profitability']['annualReturn'])
print(metrics['ratio']['sharpeRatio'])
print(metrics['risk']['maxDrawdown'])Categories:
backtest- startDate, endDate, feeRatio, taxRatio, market, freqprofitability- annualReturn, alpha, beta, avgNStock, maxNStockrisk- maxDrawdown, avgDrawdown, avgDrawdownDays, valueAtRiskratio- sharpeRatio, sortinoRatio, calmarRatio, volatilitywinrate- winRate, m12WinRate, expectancy, mae, mfeliquidity- capacity, disposalStockRatio, warningStockRatio
Other Useful Methods
# Display interactive report
report.display()
# Get trade details
trades_df = report.get_trades()
# Save to file
report.to_html("report.html")
report.to_pickle("report.pkl")
# Load from file
loaded_report = Report.from_pickle("report.pkl")
# Run specific analysis
report.run_analysis("Drawdown")
report.run_analysis("MaeMfe")
# Display ASCII chart in terminal (no Jupyter needed)
report.to_terminal()
report.to_terminal(height=8, width=60, show_benchmark=False)Key Attribute Mappings
| Desired Metric | report.metrics.X() | report.get_stats()['X'] |
|---|---|---|
| Annual Return | annual_return() | 'cagr' |
| Sharpe Ratio | sharpe_ratio() | 'monthly_sharpe' |
| Max Drawdown | max_drawdown() | 'max_drawdown' |
| Win Rate | win_rate() | 'win_ratio' |
| Total Return | - | 'total_return' |
---
Example Usage
Basic Example
import pandas as pd
from finlab import backtest
# Example position DataFrame with dates as index
position = pd.DataFrame({
'2330': [0, 1, 1],
'1101': [0.2, 0, 0],
'2454': [0.4, 0, 0]
}, index=pd.to_datetime(['2021-12-31', '2022-03-31', '2022-06-30']))
report = backtest.sim(position)
print(report)Advanced Example with Stop Loss and Take Profit
from finlab import data, backtest
close = data.get('price:收盤價')
pb = data.get('price_earning_ratio:股價淨值比')
# Define entry and exit conditions
entries = close > close.average(20)
exits = close < close.average(60)
# Create position with ranking
position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
# Backtest with stop loss and take profit
report = backtest.sim(
position,
resample='M',
stop_loss=0.1, # 10% stop loss
take_profit=0.2, # 20% take profit
name='MA Strategy with SL/TP'
)
# Display metrics
print(report.get_metrics())---
Strategy Development Workflow
Step 1: Fetching Data
Gather the necessary data using data.get(), including historical prices, volume, and any relevant indicators.
Important Notes:
- Use
with data.universe(...)ONLY to scopedata.get(...)calls; do NOT wrap position DataFrame operations - When specifying category/exclude_category, use industry names only (no numeric codes like '28')
Example:
from finlab import data
close = data.get('price:收盤價')
volume = data.get('price:成交股數')
revenue = data.get('monthly_revenue:當月營收')---
Step 2: Factor Creation
Create factors or indicators that will be used in your strategy.
Available Methods:
average,rolling(n).mean,rolling(n).std,rolling(n).max,rolling(n).minis_largest,is_smallestsustain,rise,fallindustry_rank,quantile_row- Arithmetic operators:
+,-,*,/ - Comparison operators:
==,!=,<,<=,>,>= - Logical operators:
&,|,~
Important Notes:
- Do not use
==for floating point comparisons. Usenp.isclose()instead - Be cautious with
&and|operators; ensure proper parentheses to avoid precedence issues - Prevent using reindex to align FinlabDataFrame as it already has aligned indices and columns
- Do not use for loops to iterate over rows or columns. Use vectorized operations instead
Example:
# Calculate moving averages
sma20 = close.average(20)
sma60 = close.average(60)
# Revenue growth
rev_growth = revenue.pct_change(12)
# Combine multiple conditions
strong_momentum = (close > sma20) & (close > sma60)---
Step 3: Construct Position
Define the DataFrame structure for your trading positions. The index should be a DatetimeIndex, and the columns should represent stock IDs. Use boolean values to indicate whether to hold or not, or numeric values for position sizes.
Important Notes:
- If user does not mention the sell condition, you can just use
position = a & b & c - Without sell condition, the position will be held until the end of the resample period, which is recommended, since we can set stop loss or take profit in the sim function
- If user mentions the sell condition, it is recommended to use
position = buy.hold_until(sell)where buy is(a & b & c)and sell is(a | b | c) - Use
&and|to combine multiple conditions, and use parentheses to ensure correct precedence - DO NOT use for loop to iterate over rows or columns. FinlabDataFrame already has aligned indices and columns for you
Example:
# Simple position without explicit sell condition
position = (close > sma20) & (rev_growth > 0.1)
# Position with entry and exit signals
buy = (close > sma20) & (volume > volume.average(20))
sell = (close < sma60) | (rev_growth < 0)
position = buy.hold_until(sell, nstocks_limit=10)---
Step 4: Backtest the Strategy
Test your strategy's performance and make adjustments as needed.
Associated Methods:
backtest.simreport.displayreport.get_metrics
Important Notes:
- Use the
simfunction to simulate performance based on your position DataFrame - If monthly revenue is used (as variable
rev), please setresampletorev.index - If user not mention, please set
resampleto 'ME' or 'Q' to avoid overtrading - Use
print(report.get_metrics())to extract performance metrics - If scoping tradable universe at simulation time, you may wrap the backtest call with
with data.universe(...)— but NEVER wrap factor/position calculations inside that context
Example:
from finlab import backtest
# Simple backtest
report = backtest.sim(position, resample='M')
# Backtest with universe filtering
with data.universe(market='TSE_OTC', exclude_category='金融'):
report = backtest.sim(position, resample='Q')
# Display metrics
print(report.get_metrics())
report.display()---
Lookahead Bias Self-Check — verify_strategy()
(v1.5.8) Before trusting a backtest, run the automated lookahead detector. It replays the strategy twice with truncated data and flags any positions that differ when they shouldn't — a classic signature of future data leaking into today's signal.
from finlab.verify import verify_strategy
def build_position():
# ... your strategy that returns a position DataFrame
return position
verify_strategy(build_position)If it raises, the strategy's output changed depending on data only visible in the future — fix the signal before proceeding to live trading.
---
Exception Hierarchy — finlab.exceptions
(v2.0.0) Previously most errors were raised as bare Exception or ValueError. Production code can now except on specific subclasses:
from finlab.exceptions import (
FinlabError, # root — catch-all for finlab-specific errors
DataError, # data fetch / parsing / schema
BacktestError, # sim() validation and execution
BrokerError, # broker account / order submission
AuthError, # login / token refresh
PortfolioError, # PortfolioSyncManager sync failures
ConfigError, # invalid configuration
)
try:
report = backtest.sim(position, resample='M')
except BacktestError as e:
logger.warning(f"backtest failed: {e}")
except DataError:
logger.error("data layer failure — abort run")Internally sim() was refactored (v2.0.0) into five independently testable stages — _validate_sim_inputs, _prepare_price_data, _normalize_position, _execute_simulation, _build_sim_report — so errors now originate from a narrower context, which is why most call sites raise a specific BacktestError subclass instead of a generic exception.
---
Related References
- FinlabDataFrame Reference - Learn about enhanced DataFrame methods
- Use
data.search('keyword')to explore available data sources (use Traditional Chinese keywords for TW market, English for US market) - Factor Examples - See complete strategy examples
- Factor Analysis Reference - Analyze factor performance
FinLab Best Practices and Anti-Patterns
This document contains critical coding patterns, anti-patterns, and best practices for developing FinLab strategies. Following these guidelines prevents common errors, lookahead bias, and data pollution.
Table of Contents
1. Code Patterns (DO THIS) 2. Anti-Patterns (DON'T DO THIS) 3. Preventing Future Data Pollution 4. Stock Selection Patterns 5. Backtesting Patterns 6. Error Handling
---
Code Patterns (DO THIS)
✅ Combine Conditions with Logical Operators
DO: Use &, |, ~ to combine conditions into a single position DataFrame.
from finlab import data
from finlab.backtest import sim
factor1 = data.get("price:收盤價")
factor2 = data.get("monthly_revenue:當月營收")
factor3 = data.get("price_earning_ratio:本益比")
cond1 = factor1.rank(axis=1, pct=True) > 0.5
cond2 = factor2.rank(axis=1, pct=True) > 0.5
cond_intersection = cond1 & cond2
position = factor3[cond_intersection].is_smallest(5)
report = sim(position, resample="M")DON'T: Create separate functions to generate positions (adds unnecessary complexity).
✅ Use is_smallest() or is_largest() for Stock Selection
DO: Limit to top N < 50 stocks using these methods.
# Select top 10 stocks by lowest P/E
pe = data.get("price_earning_ratio:本益比")
position = pe.is_smallest(10)
# Select top 15 stocks by highest momentum, where condition is met
close = data.get("price:收盤價")
momentum = close / close.shift(20) - 1
condition = close > close.average(60)
position = momentum[condition].is_largest(15)Note: The DataFrame used with is_smallest()/is_largest() must have float dtype, not bool. If you have a boolean condition, apply it as a filter first.
✅ Use Correct Technical Indicator Syntax
DO: Call data.indicator() without passing OHLCV data.
# Correct - no OHLCV parameters
rsi = data.indicator("RSI", timeperiod=14)
# Correct - multiple return values
macd, macd_signal, macd_hist = data.indicator(
"MACD",
fastperiod=12,
slowperiod=26,
signalperiod=9
)
# Correct - Bollinger Bands
upperband, middleband, lowerband = data.indicator(
"BBANDS",
timeperiod=20,
nbdevup=2.0,
nbdevdn=2.0,
matype=0
)DON'T: Pass close price or OHLCV data to indicators.
# ❌ WRONG - don't pass close
rsi = data.indicator("RSI", close, timeperiod=14) # ERROR✅ Use df.shift(1) for Previous Values
DO: Use .shift() to access historical data.
# Correct - get previous day's close
prev_close = close.shift(1)
# Correct - detect crossover
sma20 = close.average(20)
sma60 = close.average(60)
golden_cross = (sma20 > sma60) & (sma20.shift() < sma60.shift())DON'T: Use .iloc[-2] or similar indexing (can cause lookahead bias).
# ❌ WRONG
prev_close = close.iloc[-2] # DON'T USE THIS✅ Use data.universe() for Filtering
DO: Use context manager or set_universe() to filter stocks by market/category.
from finlab import data
# Method 1: Context manager (temporary scope)
with data.universe(market='TSE_OTC', category=['水泥工業']):
price = data.get('price:收盤價')
# Method 2: Set globally
data.set_universe(market='TSE_OTC', category='半導體', exclude_category='金融')
price = data.get('price:收盤價')Use data.search('keyword', market='<market>') to discover available datasets and data.universe() parameters. Supported markets: tw, us, kr, jp, hk. Use keywords in the dataset's native language (e.g. '營收' for tw, 'revenue' for us).
✅ Assign resample to Prevent Overtrading
DO: Always specify resample parameter in sim().
# Monthly rebalancing
sim(position, resample="M")
# Weekly rebalancing
sim(position, resample="W")
# Use monthly revenue index
rev = data.get('monthly_revenue:當月營收')
sim(position, resample=rev.index)DON'T: Omit resample (defaults to daily, causes excessive trading).
---
Anti-Patterns (DON'T DO THIS)
❌ Don't Use == for Float Comparisons
Reason: Floating point precision issues.
# ❌ BAD
condition = (close == 100.0)
# ✅ GOOD - use inequalities or np.isclose()
import numpy as np
condition = np.isclose(close, 100.0)
# Or better:
condition = (close > 99.9) & (close < 100.1)❌ Don't Use reindex() on FinLabDataFrame
Reason: FinLabDataFrame already automatically aligns indices/columns.
# ❌ BAD - unnecessary reindexing
df1 = data.get("price:收盤價")
df2 = data.get("monthly_revenue:當月營收")
df2_reindexed = df2.reindex(df1.index, method='ffill') # DON'T DO THIS
# ✅ GOOD - automatic alignment
position = df1 > df1.average(60) & (df2 > df2.shift(1))Exception: Only use reindex() for position DataFrame when changing to a specific resampling schedule:
# ✅ Allowed - reindex position to monthly revenue dates
rev = data.get('monthly_revenue:當月營收')
position_resampled = position.reindex(rev.index_str_to_date().index, method="ffill")❌ Don't Use For Loops
Reason: FinLabDataFrame methods are vectorized and much faster.
# ❌ BAD - iterating over rows
for date in close.index:
for stock in close.columns:
if close.loc[date, stock] > sma60.loc[date, stock]:
position.loc[date, stock] = True
# ✅ GOOD - vectorized operations
position = close > sma60❌ Don't Filter 注意股/處置股/全額交割股 Unless Asked
Reason: These filters remove many stocks and should only be applied when explicitly requested.
# ❌ DON'T do this by default
is_regular = (
data.get("etl:noticed_stock_filter") &
data.get("etl:disposal_stock_filter") &
data.get("etl:full_cash_delivery_stock_filter")
)
position = position & is_regular
# ✅ Only do this if user specifically asks to remove these stocks❌ Don't Pass OHLCV to Technical Indicators
Reason: data.indicator() automatically uses correct price data.
# ❌ WRONG
close = data.get("price:收盤價")
rsi = data.indicator("RSI", close, timeperiod=14) # ERROR
# ✅ CORRECT
rsi = data.indicator("RSI", timeperiod=14) # Automatically uses close❌ Don't Use Boolean Indexing with Mismatched Indices
Reason: When extracting .iloc[-1] from DataFrames with different columns, the resulting Series have different indices. Boolean indexing then fails with IndexingError.
# ❌ BAD - indices may not match
selected = latest_pe[latest_combined] # IndexingError
# ✅ GOOD - align indices first
common = latest_combined.index.intersection(latest_pe.index)
selected = latest_pe.loc[common][latest_combined.loc[common]]---
Preventing Future Data Pollution
Critical: Future data pollution (lookahead bias) occurs when you use information that wouldn't have been available at the time of decision-making. This silently corrupts backtests and makes them unrealistic.
✅ Leave df.index As-Is
DO: Keep index intact, even if it contains strings like "2025Q1".
# ✅ GOOD - leave index as-is
revenue = data.get("monthly_revenue:當月營收")
# Index may contain strings like "2022-01", "2022-02", etc.
# FinLabDataFrame aligns by shape in binary operations
position = revenue > revenue.shift(1)DON'T: Manually assign to df.index.
# ❌ FORBIDDEN - can corrupt shared data
df.index = new_index # NEVER DO THIS✅ Use Only Approved Resampling Method
DO: Use exactly this pattern for resampling (datetime index required, use .last() only).
# ✅ CORRECT resampling pattern
df = df.index_str_to_date().resample('M').last()DON'T: Use other aggregation methods like .mean(), .first(), .ffill().
# ❌ WRONG
df = df.resample('M').mean() # Can cause lookahead
df = df.resample('M').ffill() # Can cause lookahead✅ Use Only Approved Reindexing Method
DO: Use exactly method='ffill' for reindexing.
# ✅ CORRECT
df = df.reindex(target_index, method='ffill')DON'T: Use other methods like 'bfill' or None.
# ❌ WRONG
df = df.reindex(target_index, method='bfill') # Lookahead bias
df = df.reindex(target_index) # Missing data✅ Use verify_strategy() to Auto-Detect Lookahead Bias
verify_strategy() automatically tests your strategy for lookahead bias by truncating data at historical dates and comparing results against a full-data run.
Note: This is a diagnostic tool — it runs the full strategy multiple times and is slow. Only use it when the user explicitly asks to verify lookahead bias. Do NOT run it as part of routine strategy building. Requires finlab >= 1.5.8 (pip install finlab --upgrade).from finlab.verify import verify_strategy
from finlab import data
from finlab.backtest import sim
def my_strategy():
close = data.get('price:收盤價')
pb = data.get('price_earning_ratio:股價淨值比')
position = pb[close > close.average(60)].is_smallest(10)
return sim(position, resample='M', upload=False)
result = verify_strategy(my_strategy, n_tests=5)
print(result.passed) # True = no bias detected
print(result.summary_df) # Per-date test resultsParameters:
strategy(Callable, required): Zero-arg function returning aReport(output ofsim())n_tests(int, default=5): Number of random truncation dates to testtest_dates(list[str], optional): Explicit dates (YYYY-MM-DD) to test in addition to random sampleverbose(bool, default=True): Print progress and summary
Returns: VerifyResult with .passed, .n_tests, .n_passed, .n_failed, .summary_df, .details
---
Stock Selection Patterns
Pattern 1: Limit to Top X% of Indicator
# Select stocks in top 30% by momentum
momentum = close / close.shift(60) - 1
top_momentum = momentum.rank(axis=1, pct=True) > 0.7Pattern 1b: Stable Percentile Ranking with valid=
When using fillna() before ranking (e.g. to compute indicators like SLOPE), the filled values inflate the rank denominator and shift all percentiles. Use valid= to exclude them:
ratio = close / close.shift(5)
# fillna(1) needed for SLOPE, but those cells shouldn't count in rank
score = ratio.fillna(1).apply(lambda s: talib.LINEARREG_SLOPE(s, timeperiod=5))
pct = score.rank(axis=1, pct=True, valid=ratio.notna())Pattern 2: Limit to Top N Stocks
# Select top 10 stocks with lowest P/B ratio
pb = data.get("price_earning_ratio:股價淨值比")
position = pb.is_smallest(10)
# Select top 15 stocks meeting a condition
volume = data.get("price:成交股數")
liquid_stocks = volume.average(20) > 1000*1000
position = pb[liquid_stocks].is_smallest(15)Pattern 3: Entry/Exit with hold_until()
close = data.get("price:收盤價")
pb = data.get("price_earning_ratio:股價淨值比")
# Define entry and exit signals
entries = close > close.average(20)
exits = close < close.average(60)
# Hold until exit, limit to 10 stocks, rank by negative P/B
position = entries.hold_until(
exits,
nstocks_limit=10,
rank=-pb # Negative for ascending order (low P/B preferred)
)Pattern 4: Industry Ranking
# Select top 20% within each industry
roe = data.get("fundamental_features:ROE稅後")
industry_top = roe.industry_rank() > 0.8---
Backtesting Patterns
Pattern 1: Basic Backtest
sim(position, resample="M")Pattern 2: Backtest Within Date Range
sim(position.loc['2020':'2023'], resample="M")Pattern 3: Optuna Parameter Optimization
import optuna
from finlab.backtest import sim
def run_strategy(params):
"""Strategy function that returns a report"""
sma_short = close.average(params['short'])
sma_long = close.average(params['long'])
position = (sma_short > sma_long)
report = sim(position, resample="M", upload=False)
return report
def objective(trial):
params = {
'short': trial.suggest_int('short', 5, 30),
'long': trial.suggest_int('long', 40, 120)
}
report = run_strategy(params)
return report.metrics.sharpe_ratio()
# Optimize with n_trials <= 10
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=10)
print(f"Best params: {study.best_params}")Pattern 4: Evaluate Strategy Condition Coverage
# Check how often the condition is True (on average across stocks)
condition = close > close.average(60)
coverage = condition.sum(axis=1).loc['2020':].mean()
print(f"Average stocks meeting condition: {coverage:.1f}")Pattern 5: Adjust Rebalance Frequency
# Weekly
sim(position, resample="W")
# Monthly
sim(position, resample="M")
# Quarterly
sim(position, resample="Q")
# Custom: use monthly revenue index
rev = data.get('monthly_revenue:當月營收')
sim(position, resample=rev.index)Pattern 6: Adjust Rebalance Offset
# Rebalance 1 week after period start
sim(position, resample="M", resample_offset="1W")
# Rebalance 1 month after quarter start
sim(position, resample="Q", resample_offset="1M")---
Error Handling
Error: _ArrayMemoryError
Solution: Reset kernel and try again.
# Call this if you encounter _ArrayMemoryError
resetKernel()Error: requests.exceptions.ConnectionError
Solution: Reset kernel and retry.
resetKernel()Error: 用量超限 (Quota Exceeded)
常見訊息: quota exceeded, daily limit reached, 用量已達上限
解決方案:
1. 等待重置 - UTC+8 早上 8 點會自動重置用量 2. 升級方案 - 升級可獲得更多資料用量,詳見 https://www.finlab.finance/payment 3. 減少用量 - 避免重複取得相同數據,將常用數據存入變數;使用 data.universe() 限制股票範圍
Debugging Tips
1. Break down experiments into small steps
# Step 1: Fetch data
close = data.get("price:收盤價")
print(close.head())
# Step 2: Create condition
condition = close > close.average(60)
print(condition.head())
# Step 3: Select stocks
position = condition.is_largest(10)
print(position.head())2. Inspect variable values after each step to ensure correctness.
3. Use print statements to display intermediate DataFrames.
---
Strategy Design Principles
Principle 1: Be Systematic
- Good: Clearly define hypothesis, experiment setup, and evaluation criteria
- Good: Import optuna to systematically explore parameter space
- Bad: Randomly changing parameters without a clear plan
Principle 2: Start Simple
- Begin with a baseline strategy
- Add complexity incrementally
- Test each addition separately
Principle 3: Write Clear, Maintainable Code
- Use descriptive variable names
- Add comments where logic isn't self-evident
- Don't over-comment obvious operations
---
Complete Pattern Examples
Example 1: Value + Momentum + Liquidity
from finlab import data
from finlab.backtest import sim
# Fetch data
close = data.get("price:收盤價")
pb = data.get("price_earning_ratio:股價淨值比")
volume = data.get("price:成交股數")
# Create factors
value = pb.rank(axis=1, pct=True) < 0.3 # Low P/B
momentum = close.rise(20) # Rising
liquidity = volume.average(20) > 500*1000 # Liquid
# Combine
position = value & momentum & liquidity
position = pb[position].is_smallest(10)
# Backtest
report = sim(position, resample="M", stop_loss=0.08, 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%}")Example 2: Monthly Revenue Growth
from finlab import data
from finlab.backtest import sim
# Fetch revenue data
rev = data.get("monthly_revenue:當月營收")
rev_growth = data.get("monthly_revenue:去年同月增減(%)")
# Revenue momentum
rev_ma3 = rev.average(3)
rev_high = (rev_ma3 / rev_ma3.rolling(12).max()) == 1
# Sustained growth
strong_growth = (rev_growth > 20).sustain(3)
# Combine
position = rev_high & strong_growth
position = rev_growth[position].is_largest(10)
# Reindex to monthly revenue dates
position_resampled = position.reindex(rev.index_str_to_date().index, method="ffill")
# Backtest
report = sim(position_resampled, upload=False)---
See Also
- SKILL.md - Overview and quick start
- dataframe-reference.md - FinLabDataFrame methods
- backtesting-reference.md - Complete
sim()API - factor-examples.md - 60+ complete examples
FinlabDataFrame Reference
Overview
FinlabDataFrame is a powerful extension of pandas DataFrame specifically designed for financial data analysis and backtesting. It provides enhanced functionality for trading strategy development, including automatic index/column alignment, moving averages, entry/exit signal detection, and industry-based ranking.
Key Features
- Automatic re-alignment of indices and columns during arithmetic and logical operations
- Built-in methods for moving averages and technical calculations
- Entry/exit signal detection for trading strategies
- Industry-based grouping and ranking
- Multi-factor and industry neutralization
- Integration with backtesting workflows
Contents: Constructor | Index Conversion | Moving Average & Comparison | Selection | Signal Detection | Industry & Category | Neutralization | Quantile | Cross-Sectional (`.cs`) | Within-Industry (`.sector`) | Portfolio Weight (`.weight`) | Rolling Extensions | Auto Alignment | Method Chaining
---
Constructor
FinlabDataFrame
Converts a regular pandas DataFrame to a FinlabDataFrame with enhanced financial data processing capabilities.
Signature:
FinlabDataFrame(df: pd.DataFrame)Parameters:
df(pd.DataFrame, required): A pandas DataFrame to be converted to FinlabDataFrame
Returns:
- An instance of FinlabDataFrame with enhanced financial data processing capabilities
Example:
from finlab import FinlabDataFrame # top-level export (v2.0.0); `from finlab.dataframe import FinlabDataFrame` still works
from finlab import data
import pandas as pd
# Convert existing pandas DataFrame to FinlabDataFrame
regular_df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
df = FinlabDataFrame(regular_df)
# FinlabDataFrame is also automatically returned by data.get()
price_df = data.get('price:收盤價') # Returns a FinlabDataFrame---
Index Conversion Methods
index_str_to_date
Converts string-formatted financial report indices (e.g., "2022-Q1", "2022-M01") to datetime format based on actual disclosure dates. Essential for aligning financial data with daily price data.
Signature:
index_str_to_date() -> FinlabDataFrameReturns:
- FinlabDataFrame with datetime index based on actual disclosure dates
Example:
from finlab import data
# Financial statement data has string index like "2022-Q1"
cash = data.get('financial_statement:現金及約當現金')
print(cash.index[:3]) # ['2013-Q1', '2013-Q2', '2013-Q3']
# Convert to actual disclosure dates
cash_dated = cash.index_str_to_date()
print(cash_dated.index[:3]) # DatetimeIndex(['2013-05-15', '2013-08-14', ...])Note: This method uses actual financial statement disclosure dates from etl:financial_statements_disclosure_dates, not simple quarter-end dates.
---
deadline
Converts financial report indices to regulatory deadline dates (公告截止日). Unlike index_str_to_date() which uses actual disclosure dates, this uses the official filing deadlines.
Signature:
deadline() -> FinlabDataFrameReturns:
- FinlabDataFrame with datetime index based on regulatory filing deadlines
Example:
from finlab import data
# Convert quarterly data to deadline dates
cash = data.get('financial_statement:現金及約當現金')
cash_deadline = cash.deadline()
# Convert monthly revenue to deadline dates
revenue = data.get('monthly_revenue:當月營收')
revenue_deadline = revenue.deadline()Use Case: Use deadline() when you want conservative signal timing that assumes data arrives at the latest possible date. Use index_str_to_date() when you want signal timing based on actual historical disclosure.
---
Moving Average & Comparison Methods
average
Calculates a moving average over n periods. Returns NaN if more than half the values in the window are NaN.
Signature:
average(n: int) -> FinlabDataFrameParameters:
n(int, required): Number of periods for the moving average
Returns:
- FinlabDataFrame representing the moving average
Example:
from finlab import data
close = data.get('price:收盤價')
sma10 = close.average(10)
sma60 = close.average(60)
# Stock price above moving average
cond = close > sma60---
rise
Determines if values are rising compared to n periods before. Returns True if current value > value n periods ago.
Signature:
rise(n: int = 1) -> FinlabDataFrameParameters:
n(int, optional, default=1): Number of periods to compare
Returns:
- Boolean FinlabDataFrame indicating rising trends
Example:
from finlab import data
close = data.get('price:收盤價')
# Price higher than 10 days ago
rising = close.rise(10)
# Consecutive rising days
consecutive_rise = close.rise().sustain(3)---
fall
Determines if values are falling compared to n periods before. Returns True if current value < value n periods ago.
Signature:
fall(n: int = 1) -> FinlabDataFrameParameters:
n(int, optional, default=1): Number of periods to compare
Returns:
- Boolean FinlabDataFrame indicating falling trends
Example:
from finlab import data
close = data.get('price:收盤價')
# Price lower than 10 days ago
falling = close.fall(10)
# Avoid stocks in downtrend
avoid = close.fall(20)---
sustain
Checks whether a condition is sustained over a moving window of n days. Returns True if the sum of True values in the window meets or exceeds the threshold.
Signature:
sustain(nwindow: int, nsatisfy: int = None) -> FinlabDataFrameParameters:
nwindow(int, required): Window length (in days)nsatisfy(int, optional): Minimum number of True values required; defaults tonwindowif not provided
Returns:
- Boolean FinlabDataFrame
Example:
from finlab import data
close = data.get('price:收盤價')
# Price rising for 3 consecutive days
rising_3days = close.rise().sustain(3)
# Price rising at least 4 out of 5 days
rising_4of5 = close.rise().sustain(5, 4)---
Selection Methods
is_largest
Returns a boolean DataFrame where True values represent the top n largest values for each date. Eliminates the need for row-by-row iteration with nlargest.
Signature:
is_largest(n: int) -> FinlabDataFrameParameters:
n(int, required): Number of top values to select on each date
Returns:
- Boolean FinlabDataFrame with True for top n stocks on each date
Example:
from finlab import data
from finlab.backtest import sim
# Select 10 stocks with highest ROA
roa = data.get('fundamental_features:ROA稅後息前')
top_roa = roa.is_largest(10)
# Backtest holding top ROA stocks
report = sim(top_roa, resample='Q')---
is_smallest
Returns a boolean DataFrame where True values represent the n smallest values for each date.
Signature:
is_smallest(n: int) -> FinlabDataFrameParameters:
n(int, required): Number of smallest values to select on each date
Returns:
- Boolean FinlabDataFrame with True for bottom n stocks on each date
Example:
from finlab import data
from finlab.backtest import sim
# Select 10 stocks with lowest P/B ratio
pb = data.get('price_earning_ratio:股價淨值比')
lowest_pb = pb.is_smallest(10)
# Backtest value strategy
report = sim(lowest_pb, resample='M')---
rank
Computes ranking across rows or columns. Includes lookahead bias warning when ranking along the time axis.
Signature:
rank(*args, valid=None, **kwargs) -> FinlabDataFrameParameters:
- Same parameters as
pandas.DataFrame.rank() axis(int or str, default=0): Axis to rank along. Warning: axis=0 may cause lookahead biasvalid(DataFrame or Series of bool, optional): Only cells wherevalidis True participate in ranking. Cells wherevalidis False/NaN are set to NaN before ranking, so they do not affect thepct=Truedenominator. Common usage: afterfillna(), pass the originalnotna()mask to prevent newly-listed or history-insufficient stocks from polluting percentile rankings.
Returns:
- FinlabDataFrame with rankings
Example:
from finlab import data
pb = data.get('price_earning_ratio:股價淨值比')
# SAFE: Cross-sectional ranking (rank stocks against each other per day)
pb_rank = pb.rank(axis=1, pct=True)
# Select stocks in bottom 30% of P/B each day
cheap = pb_rank < 0.3
# Using valid= to exclude fillna'd stocks from rank denominator:
close = data.get('price:收盤價')
ratio = close / close.shift(5)
# fillna(1) is needed for SLOPE computation, but those fake values
# should not count in percentile ranking
score = ratio.fillna(1).apply(some_func)
score.rank(axis=1, pct=True, valid=ratio.notna())
# WARNING: Time-series ranking triggers LookaheadWarning
# This ranks each stock's current value against its future values
# pb.rank(axis=0) # Will emit warning - use rolling().rank() insteadWarning: Ranking along axis=0 (time axis) uses future data and will emit a LookaheadWarning. Use rolling().rank() or expanding().rank() for safe time-series ranking.
---
Signal Detection Methods
is_entry
Identifies entry signal points where the condition switches from False to True.
Signature:
is_entry() -> FinlabDataFrameReturns:
- Boolean FinlabDataFrame indicating entry signals
Example:
from finlab import data
close = data.get('price:收盤價')
# Condition: price in top 10
position = close.is_largest(10)
# Find days when stock enters top 10
entry_signals = position.is_entry()---
is_exit
Identifies exit signal points where the condition switches from True to False.
Signature:
is_exit() -> FinlabDataFrameReturns:
- Boolean FinlabDataFrame indicating exit signals
Example:
from finlab import data
close = data.get('price:收盤價')
# Condition: price in top 10
position = close.is_largest(10)
# Find days when stock exits top 10
exit_signals = position.is_exit()---
exit_when
Creates a position DataFrame that enters on entry signals and exits when either the original condition becomes False OR the specified exit condition becomes True.
Signature:
exit_when(exit: pd.DataFrame) -> FinlabDataFrameParameters:
exit(pd.DataFrame, required): Additional exit condition DataFrame
Returns:
- Boolean FinlabDataFrame representing positions
Example:
from finlab import data
close = data.get('price:收盤價')
volume = data.get('price:成交股數')
# Entry: price breaks above 20-day high
entry_cond = close > close.rolling(20).max().shift()
# Additional exit: volume spike (possible distribution)
volume_spike = volume > volume.average(20) * 3
# Position with additional exit condition
position = entry_cond.exit_when(volume_spike)---
hold_until
Generates trading positions based on entry signals until exit signals occur. Supports stock rotation limits, stop-loss/take-profit, and ranking-based selection.
Signature:
hold_until(
exit: pd.DataFrame,
nstocks_limit: int = None,
stop_loss: float = -np.inf,
take_profit: float = np.inf,
trade_at: str = 'close',
rank: pd.DataFrame = None
) -> FinlabDataFrameParameters:
exit(pd.DataFrame, required): Exit signal DataFramenstocks_limit(int, optional): Maximum number of stocks to hold simultaneouslystop_loss(float, optional, default=-np.inf): Stop loss threshold (e.g., 0.1 = exit if down 10%)take_profit(float, optional, default=np.inf): Take profit threshold (e.g., 0.2 = exit if up 20%)trade_at(str, optional, default='close'): Price reference for stop/take profit ('close' or 'open')rank(pd.DataFrame, optional): Ranking DataFrame for prioritizing entries when limit is reached (higher = priority)
Returns:
- Boolean FinlabDataFrame with positions (True indicates holding)
Example:
from finlab import data
from finlab.backtest import sim
close = data.get('price:收盤價')
pb = data.get('price_earning_ratio:股價淨值比')
# Entry: price above 20-day MA
entries = close > close.average(20)
# Exit: price below 60-day MA
exits = close < close.average(60)
# Hold max 10 stocks, prefer lower P/B (use negative for ascending)
position = entries.hold_until(
exits,
nstocks_limit=10,
stop_loss=0.1, # 10% stop loss
take_profit=0.3, # 30% take profit
rank=-pb # Lower P/B = higher priority
)
report = sim(position)---
Industry & Category Methods
groupby_category
Groups DataFrame columns by their industry category. Similar to pandas.DataFrame.groupby() but groups stocks by industry.
Signature:
groupby_category() -> pd.core.groupby.DataFrameGroupByReturns:
- A GroupBy object with groups defined by industry categories
Example:
from finlab import data
pb = data.get('price_earning_ratio:股價淨值比')
# Average P/B by industry
industry_pb = pb.groupby_category().mean()
# Plot semiconductor industry P/B over time
industry_pb['半導體'].plot(title='Semiconductor P/B Ratio')---
industry_rank
Calculates percentile ranking for stocks within their respective industries. Returns values from 0 (lowest in industry) to 1 (highest in industry).
Signature:
industry_rank(categories: list = None) -> FinlabDataFrameParameters:
categories(list, optional): List of industry categories to consider. If None, uses all industries fromdata.get('security_industry_themes')
Returns:
- FinlabDataFrame with industry-relative ranking scores (0 to 1)
Example:
from finlab import data
pe = data.get('price_earning_ratio:本益比')
# Rank P/E within each industry
pe_industry_rank = pe.industry_rank()
# Select stocks that are cheap relative to their industry
cheap_in_industry = pe_industry_rank < 0.3---
entry_price
Retrieves the adjusted price at entry signal points.
Signature:
entry_price(trade_at: str = 'close') -> FinlabDataFrameParameters:
trade_at(str, optional, default='close'): Price type ('close' or 'open')
Returns:
- FinlabDataFrame with entry prices (forward-filled)
Example:
from finlab import data
close = data.get('price:收盤價')
# Position signal
position = close.is_largest(10)
# Get entry prices for calculating returns
entry_prices = position.entry_price()
current_return = close / entry_prices - 1---
Neutralization Methods
neutralize
Performs cross-sectional regression to neutralize factors from the data. Returns residuals after regressing on specified neutralizer factors.
Signature:
neutralize(
neutralizers: Union[pd.DataFrame, list[pd.DataFrame], dict[str, pd.DataFrame]],
add_const: bool = True
) -> FinlabDataFrameParameters:
neutralizers(DataFrame, list, or dict, required): Factor(s) to neutralize against- Single DataFrame: neutralize against one factor
- List of DataFrames: neutralize against multiple factors
- Dict of DataFrames: neutralize with named factors
add_const(bool, optional, default=True): Whether to include intercept in regression
Returns:
- FinlabDataFrame containing regression residuals (neutralized values)
Example:
from finlab import data
# Original factor
pe = data.get('price_earning_ratio:本益比')
# Neutralize against size (market cap)
size = data.get('etl:market_value')
pe_size_neutral = pe.neutralize(size)
# Neutralize against multiple factors
pb = data.get('price_earning_ratio:股價淨值比')
pe_multi_neutral = pe.neutralize([size, pb])
# Neutralize with named factors (for clarity)
pe_named_neutral = pe.neutralize({
'size': size,
'size_squared': size ** 2,
'pb': pb
})Use Case: Factor neutralization removes unwanted exposures. For example, if you want a value factor that isn't just picking small-cap stocks, neutralize against market cap.
---
neutralize_industry
Performs cross-sectional regression to neutralize industry effects. Each stock is regressed on industry dummy variables, returning industry-neutral residuals.
Signature:
neutralize_industry(
categories: pd.DataFrame = None,
add_const: bool = True
) -> FinlabDataFrameParameters:
categories(pd.DataFrame, optional): DataFrame with 'stock_id' and 'category' columns. If None, usesdata.get('security_categories')add_const(bool, optional, default=True): Whether to include intercept (one dummy is dropped to avoid multicollinearity)
Returns:
- FinlabDataFrame containing industry-neutralized values
Example:
from finlab import data
# P/E ratio varies significantly by industry
pe = data.get('price_earning_ratio:本益比')
# Remove industry effects
pe_industry_neutral = pe.neutralize_industry()
# Now pe_industry_neutral represents deviation from industry average
# Positive = expensive relative to industry peers
# Negative = cheap relative to industry peers
# Custom industry categories
custom_cats = pd.DataFrame({
'stock_id': ['2330', '2317', '1101', '2412'],
'category': ['半導體', '電子', '水泥', '電信']
})
pe_custom_neutral = pe.neutralize_industry(categories=custom_cats)---
Quantile Methods
quantile_row
Computes the specified quantile across all stocks for each date.
Signature:
quantile_row(c: float) -> pd.SeriesParameters:
c(float, required): Quantile value between 0 and 1 (e.g., 0.9 for 90th percentile)
Returns:
- pandas Series containing the quantile value per date
Example:
from finlab import data
close = data.get('price:收盤價')
# 90th percentile price each day
q90 = close.quantile_row(0.9)
# Median price each day
median = close.quantile_row(0.5)
# Select stocks above 90th percentile
expensive = close > close.quantile_row(0.9)---
Cross-Sectional Accessor (.cs)
(v2.0.0) Cross-sectional (per-date, across stocks) transforms. Each method returns a FinlabDataFrame with the same shape.
Signature:
df.cs.rank() -> FinlabDataFrame # percentile rank per row (0~1)
df.cs.zscore() -> FinlabDataFrame # (x - mean) / std per row
df.cs.demean() -> FinlabDataFrame # x - mean per row
df.cs.winsorize(lower=0.01, upper=0.99) -> FinlabDataFrame # clip to quantile band
df.cs.bucket(n=5) -> FinlabDataFrame # equal-quantile bucketing (0..n-1)Example:
from finlab import data
pb = data.get('price_earning_ratio:股價淨值比')
# Percentile rank, winsorize tails, then z-score
factor = pb.cs.winsorize(0.01, 0.99).cs.zscore()
# Sort into 5 equal-size buckets per day
buckets = pb.cs.bucket(5)
top_bucket = buckets == 4 # cheapest bucket
# Demean to remove daily market average
factor_neutral = pb.cs.demean()Why: Consolidates common factor-engineering primitives in one place — previously these required combining rank(axis=1), manual quantile clipping, and sub(mean, axis=0).
---
Within-Industry Accessor (.sector)
(v2.0.0) Within-industry transforms and aggregations (groups stocks by industry for every date). Returns a FinlabDataFrame with the same shape.
Aggregation methods (one value per industry, broadcast back to each stock):
df.sector.mean() # industry mean
df.sector.std(ddof=1) # industry standard deviation
df.sector.median() # industry median
df.sector.sum() # industry sum
df.sector.min() # industry minimum
df.sector.max() # industry maximum
df.sector.count() # industry valid countTransform methods (same 5 as .cs but computed within industry):
df.sector.rank()
df.sector.zscore()
df.sector.demean()
df.sector.winsorize(lower=0.01, upper=0.99)
df.sector.bucket(n=5)Example:
from finlab import data
pe = data.get('price_earning_ratio:本益比')
# Z-score P/E within industry (compare against industry peers)
pe_industry_z = pe.sector.zscore()
# Industry-median P/B as a benchmark series
pb = data.get('price_earning_ratio:股價淨值比')
industry_median = pb.sector.median()
cheap_vs_industry = pb < industry_median
# Pick cheapest quintile within each industry
cheap_bucket = pb.sector.bucket(5) == 0Relationship to `industry_rank` / `neutralize_industry`: industry_rank() is equivalent to df.sector.rank(). neutralize_industry() removes industry means via regression; df.sector.demean() does the same via direct subtraction — use demean() when you just need industry-centered values and neutralize_industry() when you also want to neutralize additional factors jointly.
---
Portfolio Weight Accessor (.weight)
(v2.0.0) Post-processing transforms for position DataFrames that already contain weights (values in [-1, 1]). Input must be a weight DataFrame, not boolean signals. Each method returns a new weight DataFrame.
df.weight.cap_industry(max_weight)
df.weight.clip_by_volume(total_fund, max_participation_ratio)
df.weight.inverse_volatility(window=60)
df.weight.risk_parity(window=60)
df.weight.correlation(diversify=True)
df.weight.target_volatility(target=0.15, window=60)
df.weight.limit_turnover(max_turnover=0.5)
df.weight.drawdown_control(max_drawdown=0.1)| Method | Purpose |
|---|---|
cap_industry(max_weight) | Cap the total weight per industry; redistribute excess proportionally to other holdings |
clip_by_volume(total_fund, max_participation_ratio) | Clip per-stock weight so the traded notional stays within max_participation_ratio of average daily dollar volume |
inverse_volatility(window) | Reweight so lower-volatility stocks get higher weights (1/σ scaling) |
risk_parity(window) | Equalize each holding's risk contribution |
correlation(diversify=True) | Adjust weights using correlation structure — True diversifies, False concentrates |
target_volatility(target, window) | Scale total exposure so realized annualized vol ≈ target |
limit_turnover(max_turnover) | Cap two-way turnover between rebalance dates |
drawdown_control(max_drawdown) | De-lever when running drawdown exceeds threshold |
Example:
from finlab import data, backtest
pb = data.get('price_earning_ratio:股價淨值比')
signal = pb.is_smallest(20).astype(float) / 20 # equal-weight top-20 low-P/B
# Apply weight processing: cap industry exposure, then risk-parity within caps
weights = (
signal.weight.cap_industry(max_weight=0.3)
.weight.risk_parity(window=60)
.weight.target_volatility(target=0.12, window=60)
.weight.limit_turnover(max_turnover=0.4)
)
report = backtest.sim(weights, resample='M')Why: These used to require user-written helpers that frequently introduced lookahead bias or inconsistent handling of NaNs. Bundling them as weight-accessor methods gives a reviewed, tested implementation.
---
Rolling Extensions
(v2.0.0) FinlabDataFrame.rolling(n) now supports five additional statistical methods on top of pandas built-ins:
df.rolling(n).std() # rolling standard deviation
df.rolling(n).var() # rolling variance
df.rolling(n).skew() # rolling skewness
df.rolling(n).kurt() # rolling kurtosis
df.rolling(n).median() # rolling medianExample:
from finlab import data
close = data.get('price:收盤價')
returns = close.pct_change()
# 60-day realized volatility
vol60 = returns.rolling(60).std()
# High-skew stocks (positive tail)
skew = returns.rolling(60).skew()
lottery = skew > skew.cs.rank() > 0.8 # top 20% positive skew---
Automatic Index Alignment
FinlabDataFrame automatically aligns indices and columns when performing operations between DataFrames with different frequencies or shapes.
Supported Operations:
- Arithmetic:
+,-,*,/,//,%,** - Comparison:
>,>=,==,!=,<,<= - Logical:
&,|,^
Example:
from finlab import data
# Daily data
close = data.get('price:收盤價') # Daily frequency
# Quarterly data
roa = data.get('fundamental_features:ROA稅後息前') # Quarterly frequency
# Operations automatically align - no manual reindex needed
cond1 = close > close.average(60) # Daily condition
cond2 = roa > 0 # Quarterly condition
# Intersection works across frequencies
# Quarterly data is forward-filled to daily
position = cond1 & cond2Alignment Rules: 1. Index: Union of both indices, forward-filled 2. Columns: Intersection of both column sets 3. String indices (e.g., "2022-Q1") are converted to datetime using disclosure dates
---
Method Chaining Patterns
FinlabDataFrame methods can be chained for concise strategy expression.
Example: Complete Strategy
from finlab import data
from finlab.backtest import sim
close = data.get('price:收盤價')
pb = data.get('price_earning_ratio:股價淨值比')
roa = data.get('fundamental_features:ROA稅後息前')
# Chain conditions and selection
position = (
pb[
(close > close.average(60)) & # Above 60-day MA
(roa > 0) & # Profitable
(close.rise(5)) # Rising momentum
]
.neutralize_industry() # Industry-neutral P/B
.is_smallest(10) # Cheapest 10 stocks
)
report = sim(position, resample='M')---
Related References
- Backtesting Reference - Learn how to backtest strategies using FinlabDataFrame
- Use
data.search('keyword')to explore available data sources (use Traditional Chinese keywords for TW market, English for US market) - Factor Examples - See practical examples of using FinlabDataFrame in strategies
- Best Practices - Avoid common pitfalls including lookahead bias
Factor Analysis Reference
Overview
The FinLab factor analysis module provides comprehensive tools for evaluating factor effectiveness, calculating Information Coefficient (IC), analyzing factor trends, and computing factor contributions using Shapley values. These tools help you understand which factors drive returns and how to construct better trading strategies.
Import:
from finlab.tools.factor_analysis import (
generate_features_and_labels,
calc_factor_return,
calc_ic,
ic,
calc_metric,
calc_shapley_values,
calc_centrality,
calc_regression_stats
)Contents: Quick Start | Functions | Advanced Examples | Best Practices
---
Quick Start
Basic Factor Analysis Workflow
from finlab import data
from finlab.tools.factor_analysis import generate_features_and_labels, calc_factor_return, calc_ic
# Get data
price = data.get('etl:adj_close')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
# Generate features and labels
features, labels = generate_features_and_labels({
'marketcap': marketcap.rank(pct=True, axis=1) < 0.3,
'revenue': (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) < 0.3,
'momentum': price / price.shift(20) - 1 > 0,
}, resample=revenue.index)
# Calculate factor returns
factor_return = calc_factor_return(features, labels)
print(factor_return.head())
# Calculate IC (Information Coefficient)
ic_df = calc_ic(features, labels, rank=True)
print(ic_df.mean())---
Functions
generate_features_and_labels
Generate factor features and labels: combines factors into a feature DataFrame and generates excess return labels.
Signature:
generate_features_and_labels(
dfs: Dict[str, Union[pd.DataFrame, Callable]],
resample: str
) -> tuple[pd.DataFrame, pd.Series]Parameters:
dfs(dict, required): Factor dictionary where keys are factor names and values are DataFrame or callable functions that return DataFrame (standard input for feature.combine)resample(str, required): Resampling frequency string (e.g., 'M', 'Q', 'Y'), used for feature and label generation
Returns:
tuple[pd.DataFrame, pd.Series]: (features, labels). features has date index with columns as factor names; labels are excess returns with the same index
Example:
from finlab import data
from finlab.tools.factor_analysis import generate_features_and_labels
price = data.get('etl:adj_close')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
features, labels = generate_features_and_labels({
'marketcap': marketcap.rank(pct=True, axis=1) < 0.3,
'revenue': (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) < 0.3,
'momentum': price / price.shift(20) - 1 > 0,
}, resample=revenue.index)
print(f'Features shape: {features.shape}')
print(f'Labels shape: {labels.shape}')---
calc_factor_return
Calculate equal-weight portfolio returns based on features and labels. Automatically validates features as boolean values, calculates equal-weight portfolio returns per factor, and outputs starting from the first non-empty row.
Signature:
calc_factor_return(
features: pd.DataFrame,
labels: pd.Series
) -> pd.DataFrameParameters:
features(pd.DataFrame, required): Feature DataFrame with date index, factor names as columns, and boolean valueslabels(pd.Series, required): Label Series with date index and excess returns as values
Returns:
pd.DataFrame: Equal-weight portfolio period returns indexed by date with factor names as columns, starting from the first non-empty row
Example:
from finlab import data
from finlab.tools.factor_analysis import calc_factor_return, generate_features_and_labels
price = data.get('etl:adj_close')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
# Generate features and labels
features, labels = generate_features_and_labels({
'marketcap': marketcap.rank(pct=True, axis=1) < 0.3,
'revenue': (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) < 0.3,
'momentum': price / price.shift(20) - 1 > 0,
}, resample=revenue.index)
# Calculate factor returns
factor_return = calc_factor_return(features, labels)
print(factor_return.head())
# Analyze cumulative returns
cumulative_return = (1 + factor_return).cumprod()
cumulative_return.plot(figsize=(12, 6))---
calc_ic
Calculate the correlation coefficient (IC) between features and labels. Optionally rank features first for Rank IC. Outputs starting from the first non-empty row.
Signature:
calc_ic(
features: pd.DataFrame,
labels: pd.Series,
rank: bool = False
) -> pd.DataFrameParameters:
features(pd.DataFrame, required): Feature DataFrame with MultiIndex (date, stock_id) and factor names as columnslabels(pd.Series, required): Label Series with MultiIndex (date, stock_id)rank(bool, optional, default=False): Whether to rank features first for calculating Rank IC
Returns:
pd.DataFrame: IC values for each date and factor, starting from the first non-empty row
Example:
from finlab import data
from finlab.tools.factor_analysis import calc_ic, generate_features_and_labels
price = data.get('etl:adj_close')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
# Generate features and labels (MultiIndex: date, stock_id)
features, labels = generate_features_and_labels({
'marketcap': marketcap.rank(pct=True, axis=1) < 0.3,
'revenue': (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) < 0.3,
'momentum': price / price.shift(20) - 1 > 0,
}, resample=revenue.index)
# Calculate Rank IC
ic_df = calc_ic(features, labels, rank=True)
print(ic_df.head())
# Analyze IC statistics
print(ic_df.mean()) # Mean IC
print(ic_df.std()) # IC volatility
print(ic_df.mean() / ic_df.std()) # IC IR (Information Ratio)---
ic
Calculate Information Coefficient (IC) for factors. Internally calls calc_metric with cross-sectional correlation as the evaluation function.
Signature:
ic(
factor: pd.DataFrame | Dict[str, pd.DataFrame],
adj_close: pd.DataFrame,
days: list[int] = [10, 20, 60, 120]
) -> pd.DataFrameParameters:
factor(pd.DataFrame or dict, required): Factor data as DataFrame (columns are stock IDs) or dict[str, DataFrame] (keys are factor names)adj_close(pd.DataFrame, required): Adjusted closing price DataFrame (columns are stock IDs) for calculating future returnsdays(list[int], optional, default=[10, 20, 60, 120]): Prediction horizon list for calculating d-day future returns
Returns:
pd.DataFrame: IC for each factor at different prediction horizons. Column names are <factor>_<days>, indexed by date
Example:
from finlab import data
from finlab.tools.factor_analysis import ic
# Build factor and price
factor = data.indicator('RSI')
adj_close = data.get('etl:adj_close')
# Calculate IC (correlation coefficient)
ic_df = ic(factor, adj_close)
print(ic_df.head())
# Analyze IC at different horizons
print(ic_df.mean())
ic_df.plot(figsize=(12, 6))---
calc_metric
Calculate evaluation metrics for factors and future returns at multiple prediction horizons. Supports single DataFrame or mapping of factor names to DataFrames. Automatically aligns and trims time series.
Signature:
calc_metric(
factor: pd.DataFrame | Dict[str, pd.DataFrame],
adj_close: pd.DataFrame,
days: list[int] = [10, 20, 60, 120],
func = corr
) -> pd.DataFrameParameters:
factor(pd.DataFrame or dict, required): Factor data as DataFrame (columns are stock IDs) or dict[str, DataFrame] (keys are factor names)adj_close(pd.DataFrame, required): Adjusted closing price DataFrame (columns are stock IDs) for calculating future returnsdays(list[int], optional, default=[10, 20, 60, 120]): Prediction horizon list for calculating d-day future returnsfunc(callable, optional): Aggregation function for each date group. Takes DataFrame with 'ret' and 'f' columns and returns a single statistic. Default is corr (correlation coefficient)
Returns:
pd.DataFrame: Evaluation results for each factor at different prediction horizons. Column names are <factor>_<days>, indexed by date
Example:
from finlab import data
from finlab.tools.factor_analysis import calc_metric
# Build factor and price
factor = data.indicator('RSI')
adj_close = data.get('etl:adj_close')
# Calculate evaluation metric (default: correlation coefficient)
metric_df = calc_metric(factor, adj_close)
print(metric_df.head())
# Use custom metric function
def custom_metric(df):
# Calculate Spearman correlation
return df['f'].corr(df['ret'], method='spearman')
metric_df = calc_metric(factor, adj_close, func=custom_metric)---
calc_shapley_values
Calculate Shapley values for each factor to measure marginal contribution to portfolio performance using cooperative game theory. Enumerates all factor subsets and averages marginal contributions. Computational complexity is O(2^n) where n is the number of factors.
Signature:
calc_shapley_values(
features: pd.DataFrame,
labels: pd.Series
) -> pd.DataFrameParameters:
features(pd.DataFrame, required): Feature DataFrame with date index, factor names as columns, and boolean values (True indicates selected)labels(pd.Series, required): Label Series with MultiIndex ('datetime', 'stock_id') and excess returns as values
Returns:
pd.DataFrame: Daily Shapley values for each factor. Indexed by date with factor names as columns
Example:
from finlab import data
from finlab.tools.factor_analysis import calc_shapley_values, generate_features_and_labels
price = data.get('etl:adj_close')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
# Generate features and labels
features, labels = generate_features_and_labels({
'marketcap': marketcap.rank(pct=True, axis=1) < 0.3,
'revenue': (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) < 0.3,
'momentum': price / price.shift(20) - 1 > 0,
}, resample=revenue.index)
# Calculate Shapley values
shapley_df = calc_shapley_values(features, labels)
print(shapley_df.head())
# Analyze average contribution
print(shapley_df.mean())
shapley_df.plot(figsize=(12, 6))Note: Due to computational complexity, this function is best used with a small number of factors (typically < 10).
---
calc_centrality
Calculate rolling asset centrality for time series data. This is a generic function applicable to any DataFrame with time index and asset columns (e.g., factor returns). It is frequency-agnostic with rolling window specified by integer window_periods.
Signature:
calc_centrality(
return_df: pd.DataFrame,
window_periods: int,
n_components: int = 1
) -> pd.DataFrameParameters:
return_df(pd.DataFrame, required): Time series DataFrame indexed by date with assets (e.g., factor names) as columns. Despite the name return_df, it can be any asset time serieswindow_periods(int, required): Rolling window length in number of data points. For monthly data, 3 means 3 monthsn_components(int, optional, default=1): Number of principal components for PCA calculation
Returns:
pd.DataFrame: DataFrame with rolling centrality scores. Indexed by date (window end date) with assets as columns
Example:
import pandas as pd
from finlab.tools.factor_analysis import calc_centrality
# Assume we have factor return time series data
data = {
'FactorA': [0.1, 0.2, 0.15, 0.12, 0.11],
'FactorB': [0.05, 0.04, 0.06, 0.07, 0.08],
}
index = pd.to_datetime(['2025-01-01','2025-01-02','2025-01-03','2025-01-04','2025-01-05'])
return_df = pd.DataFrame(data, index=index)
centrality_df = calc_centrality(return_df, window_periods=3, n_components=1)
print(centrality_df.head())---
calc_regression_stats
Perform linear regression on each time series in a DataFrame and return statistics (slope, p-value, R², tail estimate, and trend classification). Uses vectorized implementation without SciPy dependency.
Signature:
calc_regression_stats(
df: pd.DataFrame,
p_value_threshold: float = 0.05,
r_squared_threshold: float = 0.1
) -> pd.DataFrameParameters:
df(pd.DataFrame, required): Time series DataFrame indexed by DatetimeIndex with different metrics as columnsp_value_threshold(float, optional, default=0.05): P-value threshold for trend significancer_squared_threshold(float, optional, default=0.1): R² threshold for trend explanatory power
Returns:
pd.DataFrame: Regression statistics for each column including slope, p_value, r_squared, tail_estimate, and trend
Example:
# Assume ic_df is a time series of factor IC
from finlab.tools.factor_analysis import calc_regression_stats
trend_stats = calc_regression_stats(ic_df)
print(trend_stats.head())
# Filter for statistically significant upward trends
significant_up = trend_stats[(trend_stats['p_value'] < 0.05) & (trend_stats['slope'] > 0)]
print(significant_up)---
Advanced Analysis Examples
Complete Factor Analysis Pipeline
from finlab import data
from finlab.tools.factor_analysis import (
generate_features_and_labels,
calc_factor_return,
calc_ic,
calc_regression_stats,
calc_shapley_values
)
import matplotlib.pyplot as plt
# 1. Define factors
price = data.get('etl:adj_close')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
pb = data.get('price_earning_ratio:股價淨值比')
# 2. Generate features and labels
features, labels = generate_features_and_labels({
'small_cap': marketcap.rank(pct=True, axis=1) < 0.3,
'revenue_growth': (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) < 0.3,
'momentum': price / price.shift(20) - 1 > 0,
'low_pb': pb.rank(pct=True, axis=1) < 0.3,
}, resample='M')
# 3. Calculate factor returns
factor_return = calc_factor_return(features, labels)
cumulative_return = (1 + factor_return).cumprod()
# 4. Calculate IC
ic_df = calc_ic(features, labels, rank=True)
print("Average IC:")
print(ic_df.mean())
# 5. Analyze IC trends
ic_trends = calc_regression_stats(ic_df)
print("\nIC Trend Statistics:")
print(ic_trends)
# 6. Calculate Shapley values (factor contributions)
shapley_df = calc_shapley_values(features, labels)
print("\nAverage Shapley Values:")
print(shapley_df.mean())
# 7. Visualization
fig, axes = plt.subplots(3, 1, figsize=(14, 12))
# Plot cumulative returns
cumulative_return.plot(ax=axes[0])
axes[0].set_title('Cumulative Factor Returns')
axes[0].set_ylabel('Cumulative Return')
axes[0].grid(True)
# Plot IC over time
ic_df.plot(ax=axes[1])
axes[1].set_title('Information Coefficient (IC) Over Time')
axes[1].set_ylabel('IC')
axes[1].axhline(y=0, color='r', linestyle='--', alpha=0.3)
axes[1].grid(True)
# Plot Shapley values
shapley_df.plot(ax=axes[2])
axes[2].set_title('Shapley Values (Factor Contributions)')
axes[2].set_ylabel('Shapley Value')
axes[2].grid(True)
plt.tight_layout()
plt.show()---
Multi-Horizon IC Analysis
from finlab import data
from finlab.tools.factor_analysis import ic
import matplotlib.pyplot as plt
# Calculate factor
rsi = data.indicator('RSI')
adj_close = data.get('etl:adj_close')
# Calculate IC at multiple horizons
ic_df = ic(rsi, adj_close, days=[5, 10, 20, 40, 60, 120])
# Analyze IC statistics
print("IC Mean:")
print(ic_df.mean())
print("\nIC Std:")
print(ic_df.std())
print("\nIC IR (Mean/Std):")
print(ic_df.mean() / ic_df.std())
# Visualization
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
# IC time series
ic_df.plot(ax=axes[0])
axes[0].set_title('IC Across Different Horizons')
axes[0].set_ylabel('IC')
axes[0].axhline(y=0, color='r', linestyle='--', alpha=0.3)
axes[0].grid(True)
# IC distribution
ic_df.plot(kind='box', ax=axes[1])
axes[1].set_title('IC Distribution Across Horizons')
axes[1].set_ylabel('IC')
axes[1].grid(True)
plt.tight_layout()
plt.show()---
Factor Combination Analysis
from finlab import data
from finlab.tools.factor_analysis import (
generate_features_and_labels,
calc_factor_return
)
import pandas as pd
# Define multiple factors
price = data.get('etl:adj_close')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
# Generate individual features
individual_features, labels = generate_features_and_labels({
'small_cap': marketcap.rank(pct=True, axis=1) < 0.3,
'revenue_growth': (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) < 0.3,
'momentum': price / price.shift(20) - 1 > 0,
}, resample='M')
# Create combined features
combined_features = pd.DataFrame(index=individual_features.index)
combined_features['small_cap'] = individual_features['small_cap']
combined_features['revenue_growth'] = individual_features['revenue_growth']
combined_features['momentum'] = individual_features['momentum']
combined_features['small_cap+revenue'] = individual_features['small_cap'] & individual_features['revenue_growth']
combined_features['all_three'] = individual_features['small_cap'] & individual_features['revenue_growth'] & individual_features['momentum']
# Calculate returns for all combinations
factor_return = calc_factor_return(combined_features, labels)
cumulative_return = (1 + factor_return).cumprod()
# Compare performance
print("Cumulative Return (Final):")
print(cumulative_return.iloc[-1])
print("\nAnnualized Return:")
print(factor_return.mean() * 12)
print("\nAnnualized Volatility:")
print(factor_return.std() * (12 ** 0.5))
print("\nSharpe Ratio:")
print((factor_return.mean() / factor_return.std()) * (12 ** 0.5))
# Visualization
cumulative_return.plot(figsize=(14, 6))
plt.title('Factor Combination Performance Comparison')
plt.ylabel('Cumulative Return')
plt.grid(True)
plt.show()---
Best Practices
1. Use Rank IC for robustness - Rank IC is more stable than raw IC 2. Analyze IC over time - Look for consistent positive IC, not just average IC 3. Check IC trend - Use calc_regression_stats to identify deteriorating factors 4. Calculate Shapley values - Understand true factor contributions in multi-factor strategies 5. Test multiple horizons - Different factors may work at different time scales 6. Combine complementary factors - Factors with low correlation often work better together 7. Monitor factor centrality - High centrality may indicate overcrowding 8. Validate out-of-sample - Always test on unseen data periods
---
Related References
- FinlabDataFrame Reference - Enhanced DataFrame methods
- Use
data.search('keyword')to discover available data sources (use Traditional Chinese keywords for TW market, English for US market) - Factor Examples - Practical factor calculations
- Machine Learning Reference - ML-based factor analysis
- Backtesting Reference - Test factor-based strategies
Factor Examples and Strategy Reference
Overview
This comprehensive guide provides practical examples of factor calculations, stock selection conditions, and complete trading strategies using the FinLab framework. All examples are organized by category for easy reference.
---
Table of Contents
1. Common Data Paths 2. Technical Indicators 3. Calculation Examples 4. Stock Selection Conditions
---
Common Data Paths
from finlab import data
# Price data
收盤價 = data.get("price:收盤價")
成交股數 = data.get("price:成交股數")
# Revenue data
當月營收 = data.get("monthly_revenue:當月營收")
去年同月增減 = data.get("monthly_revenue:去年同月增減(%)")
# Valuation metrics
本益比 = data.get("price_earning_ratio:本益比")
殖利率 = data.get("price_earning_ratio:殖利率(%)")
股價淨值比 = data.get("price_earning_ratio:股價淨值比")
# Fundamental features
ROE稅後 = data.get("fundamental_features:ROE稅後")
營業毛利率 = data.get("fundamental_features:營業毛利率")
自由現金流量 = data.get("fundamental_features:自由現金流量")
# Market value and institutional trading
市值 = data.get("etl:market_value")
外陸資買賣超股數 = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)")
投信買賣超股數 = data.get("institutional_investors_trading_summary:投信買賣超股數")---
Technical Indicators
Momentum Indicators
from finlab import data
# ADX - Average Directional Index
adx = data.indicator("ADX", adjust_price=False, resample="D", timeperiod=14)
# RSI - Relative Strength Index
rsi = data.indicator("RSI", adjust_price=False, resample="D", timeperiod=14)
# MACD - Moving Average Convergence Divergence
macd, macdsignal, macdhist = data.indicator("MACD", adjust_price=False, resample="D",
fastperiod=12, slowperiod=26, signalperiod=9)
# MOM - Momentum
mom = data.indicator("MOM", adjust_price=False, resample="D", timeperiod=10)
# ROC - Rate of Change
roc = data.indicator("ROC", adjust_price=False, resample="D", timeperiod=10)
# Stochastic Oscillator
slowk, slowd = data.indicator("STOCH", adjust_price=False, resample="D",
fastk_period=5, slowk_period=3, slowk_matype=0,
slowd_period=3, slowd_matype=0)
# Williams %R
willr = data.indicator("WILLR", adjust_price=False, resample="D", timeperiod=14)Moving Averages and Bands
# Simple Moving Average
sma = data.indicator("SMA", adjust_price=False, resample="D", timeperiod=30)
# Exponential Moving Average
ema = data.indicator("EMA", adjust_price=False, resample="D", timeperiod=30)
# Bollinger Bands
upperband, middleband, lowerband = data.indicator("BBANDS", timeperiod=20,
nbdevup=2.0, nbdevdn=2.0, matype=0)
# Weighted Moving Average
wma = data.indicator("WMA", adjust_price=False, resample="D", timeperiod=30)
# TEMA - Triple Exponential Moving Average
tema = data.indicator("TEMA", adjust_price=False, resample="D", timeperiod=30)Volatility Indicators
# ATR - Average True Range
atr = data.indicator("ATR", adjust_price=False, resample="D", timeperiod=14)
# NATR - Normalized ATR
natr = data.indicator("NATR", adjust_price=False, resample="D", timeperiod=14)
# TRANGE - True Range
trange = data.indicator("TRANGE", adjust_price=False, resample="D")Volume Indicators
# AD - Chaikin A/D Line
ad = data.indicator("AD", adjust_price=False, resample="D")
# OBV - On Balance Volume
obv = data.indicator("OBV", adjust_price=False, resample="D")
# ADOSC - Chaikin A/D Oscillator
adosc = data.indicator("ADOSC", adjust_price=False, resample="D",
fastperiod=3, slowperiod=10)---
Calculation Examples
Price-Based Calculations
from finlab import data
收盤價 = data.get("price:收盤價")
# 60-day moving average
sma = 收盤價.average(60)
# 60-day maximum price
price_max = 收盤價.rolling(60).max()
# 60-day minimum price
price_min = 收盤價.rolling(60).min()
# 20-day price change percentage
price_pct = 收盤價.pct_change(periods=20)
# Price rising compared to 60 days ago
price_rise = 收盤價.rise(60)
# Price falling compared to 60 days ago
price_fall = 收盤價.fall(60)
# Price rising for 3 consecutive days
rise_sustain = 收盤價.rise().sustain(3)
# Price rising at least 2 out of last 3 days
rise_nsatisfy = 收盤價.rise().sustain(nwindow=3, nsatisfy=2)
# Price falling for 3 consecutive days
fall_sustain = 收盤價.fall().sustain(3)
# Top 10 highest prices in market
price_largest = 收盤價.is_largest(10)
# Top 10 lowest prices in market
price_smallest = 收盤價.is_smallest(10)Volume-Based Calculations
成交股數 = data.get("price:成交股數")
# 20-day average volume
vol_ma = 成交股數.average(20)
# 20-day cumulative volume
vol_cumsum = 成交股數.rolling(20).sum()Revenue-Based Calculations
當月營收 = data.get("monthly_revenue:當月營收")
去年同月增減 = data.get("monthly_revenue:去年同月增減(%)")
# 3-month average revenue
rev_ma = 當月營收.average(3)
# Revenue YoY growth > 20% for 3 consecutive months
rev_rise_sustain = (去年同月增減 > 20).sustain(3)
# Revenue YoY growth ranking (percentile)
rev_rise_nsatisfy = 去年同月增減.rank(pct=True, axis=1)Special Calculations
# Align position to monthly revenue dates
rev = data.get("monthly_revenue:當月營收")
position = position.reindex(rev.index_str_to_date().index, method="ffill")
# Replace infinity with NaN
inf_ratio = (data.get("financial_statement:研究發展費") /
data.get("financial_statement:營業收入淨額")).replace(np.inf, np.nan)
# Inventory - use data.search('inventory') to find available ETL datasets
# Large holders (>400 lots) shareholding ratio
boss_inventory = data.get("etl:inventory:大於四百張佔比")
# Retail investors (<50 lots) shareholding ratio
small_inv = data.get("etl:inventory:小於五十張佔比")---
Stock Selection Conditions
Technical Analysis
Moving Average Strategies
收盤價 = data.get("price:收盤價")
# Price above 60-day MA
sma60 = 收盤價 > 收盤價.average(60)
# Price breaks above 60-day MA
sma60_breakout = (收盤價 > 收盤價.average(60)) & (收盤價.shift() < 收盤價.average(60).shift())
# Price breaks below 60-day MA
sma60_breakdown = (收盤價 < 收盤價.average(60)) & (收盤價.shift() > 收盤價.average(60).shift())
# Bullish alignment (5/10/20 MA)
long_ma_pattern = (收盤價 > 收盤價.average(5)) & (收盤價 > 收盤價.average(10)) & (收盤價 > 收盤價.average(20))
# Bearish alignment (5/10/20 MA)
short_ma_pattern = (收盤價 < 收盤價.average(5)) & (收盤價 < 收盤價.average(10)) & (收盤價 < 收盤價.average(20))Price Extreme Conditions
# New 5-day high
new_high = (收盤價 / 收盤價.rolling(5).max()) == 1
# New 5-day low
new_low = (收盤價 / 收盤價.rolling(5).min()) == 1
# Making new 3-day highs for 5 consecutive days
price_boost = ((收盤價 / 收盤價.rolling(3).max()) == 1).sustain(5)
# Making new 3-day lows for 5 consecutive days
price_crash = ((收盤價 / 收盤價.rolling(3).min()) == 1).sustain(5)
# Not making new 3-day highs for 5 consecutive days
price_pressure = ((收盤價 / 收盤價.rolling(3).max()) < 1).sustain(5)
# Not making new 3-day lows for 5 consecutive days
price_support = ((收盤價 / 收盤價.rolling(3).min()) > 1).sustain(5)
# 20-day price change less than 20%
price_pct_cond = 收盤價.pct_change(periods=20) < 0.20Volume Conditions
成交股數 = data.get("price:成交股數")
# 20-day average volume > 1,000,000
vol_ma = 成交股數.average(20) > 1000000
# Volume above 60-day MA
vol_ma = 成交股數 > 成交股數.average(60)
# Volume making new 3-day highs for 5 consecutive days
vol_boost = ((成交股數 / 成交股數.rolling(3).max()) == 1).sustain(5)
# Volume making new 3-day lows for 5 consecutive days
vol_crash = ((成交股數 / 成交股數.rolling(3).min()) == 1).sustain(5)Technical Indicator Conditions
# RSI golden cross
rsi1 = data.indicator("RSI", adjust_price=False, resample="D", timeperiod=14)
rsi2 = data.indicator("RSI", adjust_price=False, resample="D", timeperiod=28)
rsi_gold_cross = (rsi1 > rsi2) & (rsi1.shift() < rsi2.shift())
# RSI overbought for 5 consecutive days
rsi = data.indicator("RSI", adjust_price=False, resample="D", timeperiod=5)
rsi_high_trend = (rsi > 80).sustain(5)
# KD golden cross
slowk, slowd = data.indicator("STOCH", adjust_price=False, resample="D",
fastk_period=5, slowk_period=3, slowk_matype=0,
slowd_period=3, slowd_matype=0)
kd_gold_cross = (slowk > slowd) & (slowk.shift() < slowd.shift())
# 10-day volatility (ATR)
adj_close = data.get("etl:adj_close")
volatility = data.indicator("ATR", adjust_price=True, resample="D", timeperiod=10) / adj_close
# Breaking above Keltner Channel upper band
ema = data.indicator("EMA", adjust_price=True, resample="D", timeperiod=10)
atr = data.indicator("ATR", adjust_price=True, resample="D", timeperiod=10)
keltner_up = ema + 2 * atr
cond = (adj_close > keltner_up) & (adj_close.shift() < keltner_up.shift())
# Breaking above Bollinger upper band
upperband, middleband, lowerband = data.indicator("BBANDS", timeperiod=10)
cond = (收盤價 > upperband) & (收盤價.shift() < upperband.shift())
# Breaking below Bollinger lower band
cond = (收盤價 < lowerband) & (收盤價.shift() > lowerband.shift())
# MACD golden cross
macd, macd_signal, macd_hist = data.indicator("MACD", fastperiod=12, slowperiod=26, signalperiod=9)
macd_golden = (macd > macd_signal) & (macd.shift() < macd_signal.shift())
# MACD histogram turns positive
macd_hist_positive = (macd_hist > 0) & (macd_hist.shift() < 0)
# RSI oversold breakout (breaks above 30)
rsi = data.indicator("RSI", timeperiod=14)
rsi_oversold_breakout = (rsi > 30) & (rsi.shift() < 30)
# RSI overbought breakdown (drops below 70)
rsi_overbought_breakdown = (rsi < 70) & (rsi.shift() > 70)
# KD low-level golden cross (K < 50)
slowk, slowd = data.indicator("STOCH", fastk_period=9, slowk_period=3, slowk_matype=0, slowd_period=3, slowd_matype=0)
kd_low_golden = (slowk > slowd) & (slowk.shift() < slowd.shift()) & (slowk < 50)---
Fundamental Analysis
Revenue Growth
去年同月增減 = data.get("monthly_revenue:去年同月增減(%)")
上月比較增減 = data.get("monthly_revenue:上月比較增減(%)")
當月營收 = data.get("monthly_revenue:當月營收")
# Revenue YoY growth > 30%
rev_yy = 去年同月增減 > 30
# Revenue MoM growth > 30%
rev_mm = 上月比較增減 > 30
# 3-month average revenue > 12-month average revenue
rev_sl_compare = (當月營收.average(3) > 當月營收.average(12))
# 3-month average revenue YoY growth > 12-month average revenue YoY growth
rev_sl_growth = 當月營收.average(3).pct_change(12) > 當月營收.average(12).pct_change(12)
# 2-month average revenue at 12-month high
rev_new_high = (當月營收.average(2) / 當月營收.average(2).rolling(12, min_periods=6).max()) == 1
# At least 2 out of last 3 months with YoY growth > 20%
rev_rise_nsatisfy = (去年同月增減 > 20).sustain(nwindow=3, nsatisfy=2)
# Revenue YoY growth ranking > 80th percentile
rev_rise_nsatisfy = 去年同月增減.rank(pct=True, axis=1) > 0.80Valuation Metrics
本益比 = data.get("price_earning_ratio:本益比")
股價淨值比 = data.get("price_earning_ratio:股價淨值比")
殖利率 = data.get("price_earning_ratio:殖利率(%)")
# PE ratio between 5 and 20
pe_range = (5 <= 本益比) & (本益比 <= 20)
# PB ratio between 0.5 and 2
pb_range = (0.5 <= 股價淨值比) & (股價淨值比 <= 2)
# Dividend yield between 3% and 10%
yield_range = (3 <= 殖利率) & (殖利率 <= 10)Profitability Metrics
營運現金流 = data.get("fundamental_features:營運現金流")
營業毛利率 = data.get("fundamental_features:營業毛利率")
營業利益率 = data.get("fundamental_features:營業利益率")
稅前淨利率 = data.get("fundamental_features:稅前淨利率")
稅後淨利率 = data.get("fundamental_features:稅後淨利率")
業外收支營收率 = data.get("fundamental_features:業外收支營收率")
每股盈餘 = data.get("financial_statement:每股盈餘")
ROA綜合損益 = data.get("fundamental_features:ROA綜合損益")
ROE綜合損益 = data.get("fundamental_features:ROE綜合損益")
# Operating cash flow > 0 for 1 quarter
ope_cashflow_trend = (營運現金流 > 0).sustain(1)
# Gross margin > 3% for 1 quarter
gpm_trend = (營業毛利率 > 3).sustain(1)
# Operating margin > 3% for 1 quarter
opm_trend = (營業利益率 > 3).sustain(1)
# Pre-tax margin > 3% for 1 quarter
btpm_trend = (稅前淨利率 > 3).sustain(1)
# After-tax margin > 3% for 1 quarter
atpm_trend = (稅後淨利率 > 3).sustain(1)
# Non-operating income ratio > 3% for 1 quarter
opm_trend = (業外收支營收率 > 3).sustain(1)
# EPS > 0 for 4 consecutive quarters
eps_trend = (每股盈餘 > 0).sustain(4)
# ROA > 0% for 4 consecutive quarters
roa_trend = (ROA綜合損益 > 0).sustain(4)
# ROE > 0% for 4 consecutive quarters
roe_trend = (ROE綜合損益 > 0).sustain(4)Leverage Metrics
負債比率 = data.get("fundamental_features:負債比率")
# Debt ratio < 50% for 4 consecutive quarters
debt_trend = (負債比率 < 50).sustain(4)---
Chip Analysis
Institutional Trading
from finlab import data
# Foreign institutional net buy ratio > 10% in 1 day
iit = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)")
vol = data.get("price:成交股數")
iit_ratio = iit.rolling(1).sum() / vol.rolling(1).sum() > 0.1
# Investment trust net buy ratio > 10% in 1 day
投信買賣超股數 = data.get("institutional_investors_trading_summary:投信買賣超股數")
成交股數 = data.get("price:成交股數")
ict_ratio = 投信買賣超股數.rolling(1).sum() / 成交股數.rolling(1).sum() > 0.1
# Foreign net buy > 200,000 shares for 2 consecutive days
itt = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)")
itt_trend = (itt > 200000).sustain(2)
# Investment trust net buy > 200,000 shares for 2 consecutive days
ict_trend = (投信買賣超股數 > 200000).sustain(2)
# Three major institutional investors all buying (三大法人同買)
外資 = data.get("institutional_investors_trading_summary:外陸資買賣超股數(不含外資自營商)")
投信 = data.get("institutional_investors_trading_summary:投信買賣超股數")
自營商 = data.get("institutional_investors_trading_summary:自營商買賣超股數(自行買賣)")
三大法人同買 = (外資 > 0) & (投信 > 0) & (自營商 > 0)
連續同買 = 三大法人同買.sustain(3)
position = 外資[連續同買].is_largest(10)Shareholding Distribution
# Use data.search('inventory') to find available ETL datasets
boss_ratio = data.get("etl:inventory:大於四百張佔比")
boss800_ratio = data.get("etl:inventory:大於八百張佔比")
small_ratio = data.get("etl:inventory:小於五十張佔比")
shareholder_count = data.get("etl:inventory:全部人數")
董監持有股數占比 = data.get("internal_equity_changes:董監持有股數占比")
# Large holders (>400 lots) shareholding >= 30%
boss_inv = boss_ratio >= 30
# Large holders (>800 lots) shareholding >= 30%
boss_inv_800 = boss800_ratio >= 30
# Large holders (>400 lots) increasing for 3 consecutive periods
boss_inv_trend = boss_ratio.rise().sustain(3)
# Retail investors (<50 lots) shareholding <= 30%
small_inv = small_ratio <= 30
# Retail investors (<50 lots) decreasing for 3 consecutive periods
small_inv_trend = small_ratio.fall().sustain(3)
# Total number of shareholders decreasing for 3 consecutive periods
inv_small_people_trend = shareholder_count.fall().sustain(3)
# Director/supervisor shareholding > 30%
boss_hold = 董監持有股數占比 > 30
# Director/supervisor shareholding increasing compared to 1 month ago
boss_hold_rise = 董監持有股數占比.rise(1)Day Trading and Margin
當日沖銷交易成交股數 = data.get("intraday_trading:當日沖銷交易成交股數")
成交股數 = data.get("price:成交股數")
融資使用率 = data.get("margin_transactions:融資使用率")
融券使用率 = data.get("margin_transactions:融券使用率")
融券今日餘額 = data.get("margin_transactions:融券今日餘額")
融資今日餘額 = data.get("margin_transactions:融資今日餘額")
# Day trading ratio < 10%
day_trade_ratio = 當日沖銷交易成交股數 / 成交股數 / 2 < 0.1
# Margin utilization > 0% for 1 day
margin_used_raio = (融資使用率 > 0).sustain(1)
# Short selling utilization > 0% for 1 day
margin_sell_used_raio = (融券使用率 > 0).sustain(1)
# Short/Margin ratio > 0% for 1 day
margin_trend = (融券今日餘額 / 融資今日餘額 > 0).sustain(1)---
Market Indicators
# ADLs (Advance-Decline Line with Smoothing)
def ADLs_position(short_par=20, long_par=55):
close = data.get("price:收盤價")
close_diff = close.diff()
total_stocks = (~close.isna()).sum(1)
rise_stocks = (close_diff > 0).sum(1)
ADLs = rise_stocks / total_stocks - 0.5
short_ADLs_ma = ADLs.rolling(short_par).mean()
long_ADLs_ma = ADLs.rolling(long_par).mean()
cond = ~close.isna()
cond1 = short_ADLs_ma >= long_ADLs_ma
position = cond & cond1
return position
# VIX (Volatility Index)
def vix_position(short_par=5, long_par=20):
df = data.get("world_index:open")
vix = df["^VIX"].dropna()
short_vix_ma = vix.rolling(short_par).mean()
long_vix_ma = vix.rolling(long_par).mean()
close = data.get("price:收盤價")
cond = ~close.isna()
cond1 = short_vix_ma <= long_vix_ma
cond1 = cond1.reindex(close.index)
position = cond & cond1
return position
# Market Long/Short Alignment Count
def ls_order_position(short=5, mid=10, long=30):
close = data.get("price:收盤價")
short_ma = close.average(short)
mid_ma = close.average(mid)
long_ma = close.average(long)
long_order = (short_ma >= mid_ma) & (mid_ma >= long_ma)
long_order = long_order.sum(1)
short_order = (short_ma < mid_ma) & (mid_ma < long_ma)
short_order = short_order.sum(1)
entry = long_order > short_order
cond = ~close.isna()
position = cond & entry
return position
# Margin Maintenance Ratio
def margin_position(short_par=5, long_par=30):
融資券總餘額 = data.get("margin_balance:融資券總餘額").fillna(method="ffill")
融資今日餘額 = data.get("margin_transactions:融資今日餘額")
close = data.get("price:收盤價")
融資總餘額 = 融資券總餘額[["上市融資交易金額", "上櫃融資交易金額"]].sum(axis=1)
融資餘額市值 = (融資今日餘額 * close * 1000).sum(axis=1)[融資今日餘額.index]
mt_rate = (融資餘額市值 / 融資總餘額)
mt_rate = mt_rate.dropna()
short_ma = mt_rate.rolling(short_par).mean()
long_ma = mt_rate.rolling(long_par).mean()
entry = short_ma >= long_ma
cond = ~close.isna()
position = cond & entry
return position---
Filter Tools
from finlab import data
# Filter out attention stocks
noticed_stock_filter = data.get("etl:noticed_stock_filter")
# Filter out disposal stocks
disposal_stock_filter = data.get("etl:disposal_stock_filter")
# Filter out full cash delivery stocks
full_cash_delivery_stock_filter = data.get("etl:full_cash_delivery_stock_filter")
# Filter out KY stocks
sc = data.get("security_categories")
position_col = position.columns
ky_filter = position_col[~position_col.isin(list(sc[sc["name"].str.contains("KY")]["stock_id"]))]
position = position[ky_filter]
# Limit backtest to specific industry
data.set_universe(market="TSE_OTC", category="建材營造")---
Complete Strategy Examples
1. New High Strategy
Select stocks making 250-day new highs.
from finlab import data
from finlab.backtest import sim
close = data.get("price:收盤價")
position = (close == close.rolling(250).max())
sim(position, resample="M", name="創年新高策略")---
2. Revenue Momentum Strategy
Select stocks with strong recent revenue performance.
from finlab import data
from finlab.backtest import sim
import pandas as pd
rev = data.get("monthly_revenue:當月營收")
rev_rf = data.get("monthly_revenue:去年同月增減(%)")
vol = data.get("price:成交股數") / 1000
rev_recent_3 = rev.rolling(3).sum()
vol_avg = vol.average(10)
cond1 = (rev_recent_3 / rev_recent_3.rolling(24, min_periods=12).max()) == 1
cond2 = vol_avg > 300
cond_all = cond1 & cond2
result = rev_rf * (cond_all)
position = result[result > 0].is_largest(10).reindex(rev.index_str_to_date().index, method="ffill")
sim(position=position, stop_loss=0.3, position_limit=0.1)---
3. Cash Flow Strategy
Select stocks with positive cash flows across all categories.
from finlab import data
from finlab.backtest import sim
營業現金流 = data.get("financial_statement:營業活動之淨現金流入_流出")
投資現金流 = data.get("financial_statement:投資活動之淨現金流入_流出")
融資現金流 = data.get("financial_statement:籌資活動之淨現金流入_流出")
position = (營業現金流 > 0) & (投資現金流 > 0) & (融資現金流 > 0)
report = sim(position, resample="M", name="現金流正數")---
4. PEG Strategy
Price-Earnings to Growth ratio strategy.
from finlab import data
from finlab.backtest import sim
pe = data.get("price_earning_ratio:本益比")
rev = data.get("monthly_revenue:當月營收")
rev_ma3 = rev.average(3)
rev_ma12 = rev.average(12)
營業利益成長率 = data.get("fundamental_features:營業利益成長率")
peg = (pe / 營業利益成長率)
cond1 = rev_ma3 / rev_ma12 > 1.1
cond2 = rev / rev.shift(1) > 0.9
cond_all = cond1 & cond2
result = peg * (cond_all)
position = result[result > 0].is_smallest(10).reindex(rev.index_str_to_date().index, method="ffill")
sim(position=position, name="peg_rev", fee_ratio=1.425/1000/3, stop_loss=0.1)---
5. Momentum + ROE Filter Strategy
Combine price momentum with ROE filter.
from finlab import data
from finlab.backtest import sim
# Download ROE and closing price
roe = data.get("fundamental_features:ROE稅後")
close = data.get("price:收盤價")
position = ((close / close.shift(60)).is_largest(30) & (roe > 0))
# Backtest, rebalance monthly (M)
report = sim(position, resample="M")---
6. Low PB Strategy
Price-to-Book ratio strategy with technical filter.
from finlab import data
from finlab.backtest import sim
pb = data.get("price_earning_ratio:股價淨值比")
close = data.get("price:收盤價")
buy = (1 / (pb * close) * (close > close.average(60)) * (close > 5)).is_largest(20)
sim(buy, resample="Q")---
7. Triple RSI Strategy
Advanced RSI-based strategy with multiple timeframes.
from finlab import data
from finlab.backtest import sim
import pandas as pd
from finlab import dataframe
close = data.get("price:收盤價")
roe = data.get("fundamental_features:ROE稅後")
rsi1 = data.indicator("RSI", timeperiod=20)
rsi2 = data.indicator("RSI", freq="D", timeperiod=60)
rsi3 = data.indicator("RSI", freq="D", timeperiod=120)
buy = (rsi3 > 55) & (rsi1 / rsi1.shift(3) > 1.02) & (roe > 0) & \
dataframe.FinlabDataFrame(rsi1 > 75).sustain(3) & (rsi2 < 75)
sell = buy.shift(60) | (close < close.average(60))
position = pd.DataFrame(np.nan, index=buy.index, columns=buy.columns)
position[buy] = 1
position[sell] = 0
position = position.ffill().fillna(0)
report = sim(position.loc["2014":], resample="W")---
8. High RSI Strategy
Simple high RSI momentum strategy.
from finlab import data
from finlab.backtest import sim
rsi = data.indicator("RSI")
position = rsi.is_largest(20)
report = sim(position, resample="W", name="高RSI策略")---
9. Entry/Exit Signal Example
Using hold_until for explicit entry and exit signals.
from finlab import data
from finlab.backtest import sim
close = data.get("price:收盤價")
pb = data.get("price_earning_ratio:股價淨值比")
sma20 = close.average(20)
sma60 = close.average(60)
entries = close > sma20
exits = close < sma60
position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
sim(position)---
10. Long/Short Strategy
Example of simultaneous long and short positions.
from finlab import data
from finlab import backtest
close = data.get("price:收盤價")
position = close < 0 # Start with all False
position["2330"] = 0.5 # Long TSMC with 50% weight
position["1101"] = -0.5 # Short stock 1101 with 50% weight
report = backtest.sim(position)---
Best Practices
1. Use vectorized operations - Never use for loops on FinlabDataFrame 2. Set appropriate resample periods - Use 'M', 'Q', or revenue.index to avoid overtrading 3. Combine multiple factors - Single factor strategies are often less robust 4. Apply filters - Remove special status stocks (disposal, attention, full cash delivery) 5. Control position size - Use position_limit and nstocks_limit 6. Set stop loss/take profit - Protect against large losses 7. Universe filtering - Use data.universe() to scope data.get() calls only 8. Proper alignment - Let FinlabDataFrame handle index/column alignment automatically
---
US Equity Examples
The patterns above use TW-market data paths (price:收盤價, etc.). On US market the surface area is the same — is_largest, rank, rolling, sim(), Report — only the data paths, the active market, and fee/tax defaults change. Four syntax-only templates; read us-market.md for data availability, alignment, and universe-construction guidance.
data.get() does not take a market= argument. Select the market with data.set_market('us') (single-name equities) or data.set_market('us_fund') (ETFs / mutual funds) before calling data.get() / sim().
Dollar-volume top-N momentum (single-name equities)
from finlab import data
from finlab.backtest import sim
data.set_market('us')
close = data.get('us_price:adj_close')
volume = data.get('us_price:volume')
# 6-month momentum, top-100 dollar-volume universe, weekly rebalance
dollar_vol = (close * volume).rolling(60, min_periods=20).mean()
top_100 = dollar_vol.is_largest(100)
momentum = close / close.shift(126) - 1
position = momentum[top_100].is_largest(20)
report = sim(position, resample='W') # USMarket fee/tax defaults apply
print(f"CAGR: {report.get_stats()['cagr']:.2%}")S&P 500 quality screen (post-2022-11 only)
from finlab import data
from finlab.backtest import sim
data.set_market('us')
with data.us_universe(index='S&P 500'):
close = data.get('us_price:adj_close')
ocf = data.get('us_cash_flow:operating_cash_flow') # quarterly, filing-date aligned
eps = data.get('us_income_statement:eps_diluted')
# Quality: positive OCF and rising trailing EPS
quality = (ocf > 0) & (eps > eps.shift(4))
position = quality & (close > close.average(200))
report = sim(position, resample='M')ETF rotation (SPY vs. QQQ)
Switch the market to us_fund when the tradable universe is ETFs or funds. ETF tickers are not in us_price; only us_fund_price carries them.
from finlab import data
from finlab.backtest import sim
data.set_market('us_fund')
close = data.get('us_fund_price:adj_close')
momentum = close[['SPY', 'QQQ']].pct_change(126)
position = momentum.is_largest(1) # hold the single strongest of the two
report = sim(position, resample='M')Computing a ratio from raw statements (avoids current-snapshot tables)
from finlab import data
# Do NOT use us_ratios:pe — it only has ~16 days of history.
# Compute trailing-twelve-months P/E from raw fundamentals instead.
data.set_market('us')
price = data.get('us_price:adj_close')
eps_q = data.get('us_income_statement:eps_diluted') # quarterly
ttm_eps = eps_q.rolling(4).sum()
pe = price / ttm_eps
cheap = pe.rank(axis=1, pct=True) < 0.3See us-market.md §1.2 for the full list of current-snapshot tables to avoid in backtests, and §4.2 for the full USFundMarket ETF workflow.
---
Related References
- FinlabDataFrame Reference - Enhanced DataFrame methods
- Backtesting Reference - Backtest your strategies
- Use
data.search('keyword')to explore the complete data catalog (use Traditional Chinese keywords for TW market, English for US market) - Factor Analysis Reference - Analyze factor performance
- Machine Learning Reference - ML-based strategies
- US Market Reference - Data map, defaults, universe construction for US equity