FinlabDataFrame: Auto-Alignment and Stock Selection API
FinlabDataFrame extends Pandas DataFrame for stock strategy development. Most datasets returned by data.get() use this type, so prices, monthly revenue, and quarterly fundamentals can be combined directly without manual reindex work.
| Task | Main method | Example |
|---|---|---|
| Align daily, monthly, and quarterly data | Automatic alignment | (close > close.average(20)) & (revenue > 1e8) |
| Select the top or bottom N stocks | is_largest() / is_smallest() |
marketcap.is_smallest(30) |
| Build positions with stop-loss or take-profit exits | hold_until() |
position.hold_until(stop_loss=0.1) |
| Run cross-sectional and sector transforms | df.cs / df.sector |
factor.cs.rank() |
| Construct portfolio weights | df.weight |
position.weight.equal() |
If you are new to data.get(), start with finlab.data. For a full strategy workflow, continue with Research to Production.
Use Cases
- Automatically align data of different frequencies (daily, weekly, monthly, quarterly)
- Quickly perform stock selection (ranking, top-N filtering)
- Compute moving averages and technical indicators
- Manage entry/exit signals (hold until stop-loss/take-profit)
- Industry neutralization
- Cross-sectional factor preprocessing (
df.cs: rank, winsorize, z-score) - Within-sector transforms and aggregation (
df.sector: sector rank, mean, std) - Portfolio weight construction (
df.weight: inverse volatility, risk parity, drawdown control)
Quick Examples
Auto-Alignment and Stock Selection
from finlab import data
# data.get() returns FinlabDataFrame
close = data.get('price:收盤價') # Daily
revenue = data.get('monthly_revenue:當月營收') # Monthly
# Automatic date alignment (revenue is forward-filled to daily frequency)
position = (close > close.average(20)) & (revenue > 1e8)
Ranking and Filtering
from finlab import data
close = data.get('price:收盤價')
marketcap = data.get('etl:market_value')
# Select the 30 smallest by market cap
small_cap = marketcap.is_smallest(30)
# Select the 20 largest by price momentum
momentum = (close / close.shift(20)).is_largest(20)
# Combine strategies
position = small_cap & momentum
Position Management
from finlab import data
from finlab.backtest import sim
close = data.get('price:收盤價')
position = close > close.average(20)
exit_condition = close < close.average(60)
# Hold until exit condition, 10% stop-loss, or 20% take-profit
position_with_stops = position.hold_until(
exit=exit_condition,
stop_loss=0.1,
take_profit=0.2,
)
# Backtest
report = sim(position_with_stops, resample='M')
report.display()
FinlabDataFrame vs pandas DataFrame
| Feature | pandas DataFrame | FinlabDataFrame |
|---|---|---|
| Basic operations | Fully supported | Fully supported (inherits pandas) |
| Auto-alignment | Requires manual handling | Automatically aligns different frequency data |
| Stock selection | Write your own | Built-in is_largest(), is_smallest() |
| Technical indicators | Requires extra packages | Built-in average(), rise(), fall() |
| Position management | Write your own | Built-in hold_until(), exit_when() |
| Industry analysis | Write your own | Built-in industry_rank(), neutralize_industry() |
| Factor preprocessing | Write your own | Built-in df.cs cross-sectional transforms |
| Within-sector analysis | Write your own | Built-in df.sector sector transforms & aggregation |
| Weight construction | Write your own | Built-in df.weight with 8 weighting methods |
API Reference
FinlabDataFrame
finlab.dataframe.FinlabDataFrame
Bases: IndexConversionMixin, SignalMixin, NeutralizeMixin, IndustryMixin, DataFrame
回測語法糖
除了使用熟悉的 Pandas 語法外,我們也提供很多語法糖,讓大家開發程式時,可以用簡易的語法完成複雜的功能,讓開發策略更簡潔!
我們將所有的語法糖包裹在 FinlabDataFrame 中,用起來跟 pd.DataFrame 一樣,但是多了很多功能!
只要使用 finlab.data.get() 所獲得的資料,皆為 FinlabDataFrame 格式,
接下來我們就來看看, FinlabDataFrame 有哪些好用的語法糖吧!
當資料日期沒有對齊(例如: 財報 vs 收盤價 vs 月報)時,在使用以下運算符號:
+, -, *, /, >, >=, ==, <, <=, &, |, ~,
不需要先將資料對齊,因為 FinlabDataFrame 會自動幫你處理,以下是示意圖。

以下是範例:cond1 與 cond2 分別為「每天」,和「每季」的資料,假如要取交集的時間,可以用以下語法:
from finlab import data
# 取得 FinlabDataFrame
close = data.get('price:收盤價')
roa = data.get('fundamental_features:ROA稅後息前')
# 運算兩個選股條件交集
cond1 = close > 37
cond2 = roa > 0
cond_1_2 = cond1 & cond2
cond1 跟 cond2 訊號的頻率雖然不相同,
但是由於 cond1 跟 cond2 是 FinlabDataFrame,所以可以直接取交集,而不用處理資料頻率對齊的問題。
總結來說,FinlabDataFrame 與一般 dataframe 唯二不同之處:
1. 多了一些 method,如df.is_largest(), df.sustain()...等。
2. 在做四則運算、不等式運算前,會將 df1、df2 的 index 取聯集,column 取交集。
與 pd.Series 運算時,依 Series 的 index 判斷對齊方向:日期(或季/月字串)
index 對齊日期;股票代號 index 對齊股票、套用到每一天(& / | 缺少的股票視為
False,四則運算為 NaN);兩者皆符合或皆不符合時會報錯,請改用
df.mul(s, axis=0)(日期)或 df.mul(s, axis=1)(股票)明確指定。
日期布林 Series 擇時:position[cond] 與 position & cond 都保留每個日期,
False 的日期空手。
align
pd.DataFrame.align that converts a quarterly side to dates first.
average
取 n 筆移動平均
若股票在時間窗格內,有 N/2 筆 NaN,則會產生 NaN。 Args: n (positive-int): 設定移動窗格數。 Returns: (pd.DataFrame): data Examples: 股價在均線之上
只需要簡單的語法,就可以將其中一部分的訊號繪製出來檢查:import matplotlib.pyplot as plt
close.loc['2021', '2330'].plot()
sma.loc['2021', '2330'].plot()
cond.loc['2021', '2330'].mul(20).add(500).plot()
plt.legend(['close', 'sma', 'cond'])
corrwith
Compute pairwise correlations with pandas alignment and arithmetic.
fall
is_largest
取每列前 n 筆大的數值
若符合 True ,反之為 False 。用來篩選每天數值最大的股票。
Args:
n (positive-int): 設定每列前 n 筆大的數值。
Returns:
(pd.DataFrame): data
Examples:
每季 ROA 前 10 名的股票
from finlab import data
roa = data.get('fundamental_features:ROA稅後息前')
good_stocks = roa.is_largest(10)
只在符合條件的股票中排名:用 `score[cond]` 排除不符合的股票,
不要用 `cond * score`。後者讓不符合的股票分數為 0,符合的股票
少於 n 檔時會被補進來,並發出 `TopNPaddingWarning`。
```py
top = score[cond].is_largest(10)
```
is_smallest
取每列前 n 筆小的數值
若符合 True ,反之為 False 。用來篩選每天數值最小的股票。
Args:
n (positive-int): 設定每列前 n 筆小的數值。
Returns:
(pd.DataFrame): data
Examples:
股價淨值比最小的 10 檔股票
from finlab import data
pb = data.get('price_earning_ratio:股價淨值比')
cheap_stocks = pb.is_smallest(10)
只在符合條件的股票中排名時用 `pb[cond].is_smallest(10)`。
每列同為最高值的股票多於剩餘名額、只能依欄位順序挑選時,
會發出 `TopNPaddingWarning`。
lazy
Wrap this DataFrame into a lazy computation handle.
Returns a LazyWide proxy that records operations without
executing them. Call .rebalance(freq) or .collect()
on the result to trigger execution at specific dates only.
Example::
close = data.get('price:收盤價')
position = (
close.lazy()
.rolling(60).mean()
.rank(axis=1, pct=True)
.is_largest(10)
.rebalance('M')
)
quantile_row
rank
Cross-sectional or time-series ranking.
| PARAMETER | DESCRIPTION |
|---|---|
valid
|
只有 valid 為 True 的 cell 才參與排名。 valid 為 False/NaN 的 cell 在排名前設為 NaN, 因此不影響 pct=True 的分母。 常見用法:fillna 後傳入原始 notna() mask, 避免新上市或歷史不足的股票污染百分位排名。
TYPE:
|
rebalance
Downsample to keep only the last observation per period.
Equivalent to self.resample(freq).last().
| PARAMETER | DESCRIPTION |
|---|---|
freq
|
Pandas frequency string, e.g.
TYPE:
|
reindex
pd.DataFrame.reindex that understands quarterly string indexes.
Reindexing a quarterly ('2013-Q1') FinlabDataFrame onto dates first
converts the quarters to disclosure dates with index_str_to_date(),
so df.reindex(close.index, method='ffill') gives each stock's
latest disclosed value instead of all NaN or a TypeError.
Key FinlabDataFrame Methods:
Stock Selection and Ranking
| Method | Description | Example |
|---|---|---|
is_largest(n) |
Select the N largest values | marketcap.is_largest(30) |
is_smallest(n) |
Select the N smallest values | pe_ratio.is_smallest(20) |
rank() |
Rank across stocks per date | close.rank(axis=1, pct=True) |
industry_rank() |
Ranking within industry | close.industry_rank() |
Technical Indicators
| Method | Description | Example |
|---|---|---|
average(n) |
N-period moving average | close.average(20) |
rise(n) |
Consecutive N-period rise | close.rise(3) |
fall(n) |
Consecutive N-period fall | close.fall(2) |
sustain(n) |
Condition sustained for N periods | (close > ma20).sustain(5) |
Position Management
| Method | Description | Example |
|---|---|---|
hold_until() |
Hold until exit condition | position.hold_until(close < ma20) |
exit_when() |
Exit on condition | position.exit_when(close < ma20) |
entry_price() |
Get entry price | position.entry_price('close') |
Industry Analysis
| Method | Description | Example |
|---|---|---|
neutralize_industry() |
Industry neutralization | factor.neutralize_industry() |
groupby_category() |
Group by industry | close.groupby_category().mean() |
average()
Compute moving average.
Usage Examples:
from finlab import data
close = data.get('price:收盤價')
# 20-day moving average
ma20 = close.average(20)
# 60-day moving average
ma60 = close.average(60)
# Moving average crossover strategy
golden_cross = ma20 > ma60
Difference from pandas rolling()
average(20)is equivalent torolling(20).mean()- More concise and readable
- Return value is still a FinlabDataFrame, allowing method chaining
is_largest() / is_smallest()
Select top/bottom N by ranking.
Usage Examples:
from finlab import data
close = data.get('price:收盤價')
marketcap = data.get('etl:market_value')
# Select the 30 smallest by market cap (small-cap stocks)
small_cap = marketcap.is_smallest(30)
# Select the 20 largest by momentum (momentum stocks)
momentum = (close / close.shift(20)).is_largest(20)
# Combine: momentum stocks among small-caps
position = small_cap & momentum
Notes
is_largest(n)re-ranks every period (daily/monthly)- If fewer than N stocks are available, all are selected
- Returns a Boolean DataFrame (True = selected)
To rank only the stocks that pass a condition, exclude the others with score[cond] (or score.where(cond)):
# Stocks failing cond are NaN and not ranked; if fewer than 10 pass, only those are selected
position = score[cond].is_largest(10)
(cond * score).is_largest(10) gives stocks failing cond a score of 0. When fewer than 10 stocks pass, those 0-score stocks fill the slots in column order (1101, 1102, 1103, ...). When a row fills its slots with stocks tied at its lowest score (highest for is_smallest), finlab emits a TopNPaddingWarning; the selection is unchanged.
rank()
Compute ranking (supports percentile ranking). Use axis=1 to rank across stocks on each date; for within-industry ranking, use industry_rank().
Usage Examples:
from finlab import data
close = data.get('price:收盤價')
marketcap = data.get('etl:market_value')
# Compute percentile ranking (0-1)
cap_rank = marketcap.rank(pct=True, axis=1)
# Select top 30% by market cap
position = cap_rank > 0.7
# Or use is_largest (more intuitive)
position = marketcap.is_largest(int(len(marketcap.columns) * 0.3))
industry_rank()
Ranking within industry.
Usage Examples:
from finlab import data
close = data.get('price:收盤價')
# Compute within-industry ranking (0-1)
industry_rank = close.industry_rank()
# Select stocks with an industry percentile score above 0.7
position = industry_rank > 0.7
Industry Classification Source
- For Taiwan stocks (
tw_stockandrotc_stock), classifications come from the TPEx Industry Value Chain Information Platform viadata.get('security_industry_themes'). - With
categories=None, only top-level categories (names without:) are used; passcategories=[...]to pick specific ones, including subcategories. - Ranked per date with
rank(axis=1, pct=True)within each category; stocks in multiple categories get the mean percentile.
Classification snapshot, not historical as-of data
security_industry_themes is the latest snapshot and is applied to every date, so historical rankings may use classifications not yet known at the time. Keep this in mind when backtesting.
rise() / fall()
Detect consecutive rises/falls.
Usage Examples:
from finlab import data
close = data.get('price:收盤價')
# 3 consecutive days of rising
rising = close.rise(3)
# 2 consecutive days of falling
falling = close.fall(2)
# Strategy: reverse signal after consecutive rises
position = ~rising # Inverse signal
sustain()
Check if a condition is sustained for N periods.
Usage Examples:
from finlab import data
close = data.get('price:收盤價')
ma20 = close.average(20)
# Price stays above 20-day MA for 5 consecutive days
sustained_above_ma = (close > ma20).sustain(5)
# Strategy
position = sustained_above_ma
sustain vs rise
rise(n): value increases for n consecutive periodssustain(n): condition is True for n consecutive periods
hold_until()
Hold position until an exit condition, stop-loss, take-profit, or trailing stop triggers.
Usage Examples:
from finlab import data
from finlab.backtest import sim
close = data.get('price:收盤價')
position = close > close.average(20)
exit_condition = close < close.average(60)
# Basic usage: exit condition
position_with_exit = position.hold_until(exit_condition)
# Exit condition + 10% stop-loss + 20% take-profit
position_with_stops = position.hold_until(
exit=exit_condition,
stop_loss=0.1,
take_profit=0.2,
)
# Exit condition + 10% trailing stop, activated only after +20% profit
position_with_trail = position.hold_until(
exit=exit_condition,
trail_stop=0.1,
trail_stop_activation=0.2,
)
# Backtest
report = sim(position_with_stops, resample='M')
report.display()
Key Concepts
hold_until()extends the holding period until the trigger condition- Stop-loss/take-profit are calculated based on the entry price
trail_stopexits when return falls from the holding-period peak by the configured amount;trail_stop_activationcan require a minimum profit before the trailing stop activates
Exit signals as a state or as events (exit_mode)
Entry signals act only on their own dates. When the exit has sparser dates than the entries (monthly revenue, financial statements, weekly/monthly conditions), exit_mode decides what the dates missing from exit mean:
exit_mode |
Dates missing from exit |
Suited to |
|---|---|---|
'state' (default) |
Keep the previous row's value until the next row | Low-frequency conditions such as monthly revenue or financial statements: once published, the exit holds and the stock is not re-entered |
'event' |
No exit | Data that lists event dates only: exit on the dates exit has, then buy again whenever the entry is True |
from finlab import data
close = data.get('price:收盤價')
entries = close > close.average(20)
yoy = data.get('monthly_revenue:去年同月增減(%)')
# Exit when revenue YoY is negative; re-enter only after a later release is no longer negative
position = entries.hold_until(yoy < 0)
# Exit only on the dates monthly revenue has
position = entries.hold_until(yoy < 0, exit_mode='event')
'state' keeps the last row
If the last row of exit is True and no False follows, the stock is never entered again. A single event on one date (for example a DataFrame that lists event dates only) also keeps blocking entries; use exit_mode='event' for such data.
NaN means no exit in both modes. 'state' fills only the dates missing from exit and never replaces a NaN with an earlier True. 'state' needs the exit dates sorted from old to new and otherwise raises ValueError; call exit.sort_index() first.
When both modes give the same result
Both modes give the same result when exit has a row on every entries date, for example when both come from the same daily price table; quarterly string indexes are compared after conversion to dates. Extra dates added by rank or by the prices for stop-loss/take-profit carry no entry signals and don't matter. A daily exit that misses an entries date, for example because it updates one day later or skips a trading day, can differ: on that date 'state' keeps the previous row and 'event' treats it as no exit.
'event' only sees the dates of the exit it receives
In these cases the exit passed to hold_until has already been forward-filled onto a denser timeline. Each filled row counts as an event of its own, so 'event' cannot recover exits that originally appeared only on release dates:
- A condition combined with daily data, for example
(yoy < 0) | (close < close.average(60)): the result equals'state'. To treat it as events, pass the original monthly condition. - A quarterly string index (such as
2024-Q1):index_str_to_date()indexes by every company's disclosure dates and carries each company's value onto the other companies' disclosure dates, so while that value is True,'event'also exits on other companies' disclosure dates, not only on the company's own. - A lazy
exit(fromdata.get(..., lazy=True)or.lazy()): the lazy engine forward-fills it onto the lazy timeline before callinghold_until, soexit_mode='event'silently gives the same result as'state'. For event semantics, pass an eagerexit, or call.collect()on the low-frequency exit condition alone first:
# Eager exit condition (data fetched without lazy=True)
position = entries.lazy().hold_until(yoy < 0, exit_mode='event').collect()
# A lazy exit condition: collect it on its own first
yoy_lazy = data.get('monthly_revenue:去年同月增減(%)', lazy=True)
exits = (yoy_lazy < 0).collect()
position = entries.lazy().hold_until(exits, exit_mode='event').collect()
The default was 'event' in 2.1.0–2.2.0 and 'state' before 2.1.0 (through 2.0.23) and from now on. To keep 2.1.0–2.2.0 positions, add exit_mode='event'.
exit_when()
Exit when a specific condition is met.
Usage Examples:
from finlab import data
close = data.get('price:收盤價')
ma20 = close.average(20)
ma60 = close.average(60)
# Entry: short MA crosses above long MA
entry = ma20 > ma60
# Exit: short MA crosses below long MA
exit_signal = ma20 < ma60
# Hold until exit signal
position = entry.exit_when(exit_signal)
exit_when vs hold_until
exit_when(): Custom exit conditions (e.g., technical indicators)hold_until(): Fixed stop-loss/take-profit percentages
neutralize_industry()
Industry neutralization - removes industry factor effects.
Usage Examples:
from finlab import data
# Raw factor (with industry influence)
factor = data.get('fundamental_features:股東權益報酬率')
# Industry neutralization (removes industry average)
neutral_factor = factor.neutralize_industry()
# Use neutralized factor
position = neutral_factor.is_largest(30)
Why Neutralize?
- Some industries naturally have higher ROE (e.g., financial sector)
- Neutralization avoids over-concentration in a single industry
- Improves strategy stability
Portfolio Weight Construction df.weight
The df.weight accessor provides methods for constructing and adjusting portfolio weights. All methods return FinlabDataFrame and support method chaining.
Method Overview
| Method | Description | Example |
|---|---|---|
cap_industry(max_weight) |
Cap industry weight, redistribute excess proportionally | w.weight.cap_industry(0.3) |
clip_by_volume(total_fund, max_participation_ratio, volume, window) |
Clip stock weight by average daily volume | w.weight.clip_by_volume(1e7) |
inverse_volatility(window, price) |
Inverse volatility weighting — low-vol stocks get higher weight | w.weight.inverse_volatility(60) |
risk_parity(window, price) |
Risk parity weighting — equal risk contribution per holding | w.weight.risk_parity(60) |
correlation(window, price, diversify) |
Adjust weights by correlation — diversify or concentrate | w.weight.correlation(diversify=True) |
target_volatility(target, window, price) |
Scale weights to achieve target annualized volatility | w.weight.target_volatility(0.15) |
limit_turnover(max_turnover) |
Cap two-way turnover between rebalances | w.weight.limit_turnover(0.3) |
drawdown_control(max_drawdown, price) |
Reduce exposure when drawdown exceeds threshold | w.weight.drawdown_control(0.15) |
Usage Example
from finlab import data
from finlab.backtest import sim
close = data.get('price:收盤價')
marketcap = data.get('etl:market_value')
# Stock selection
position = marketcap.is_smallest(30)
# Chain weight construction
weighted = (
position.weight.inverse_volatility(window=60) # Inverse volatility weighting
.weight.cap_industry(max_weight=0.3) # Industry cap at 30%
.weight.limit_turnover(max_turnover=0.3) # Turnover cap at 30%
.weight.drawdown_control(max_drawdown=0.15) # Drawdown control at 15%
)
report = sim(weighted, resample='M')
report.display()
Method ordering
- Apply weighting first (inverse_volatility / risk_parity), then constraints (cap_industry / clip_by_volume)
limit_turnoveranddrawdown_controlare typically applied last
Cross-Sectional Transforms df.cs
The df.cs (cross-sectional) accessor provides transforms computed across all stocks on each date.
Method Overview
| Method | Description | Example |
|---|---|---|
rank() |
Percentile rank (0, 1] | factor.cs.rank() |
winsorize(lower, upper) |
Percentile winsorization | factor.cs.winsorize(0.05, 0.95) |
bucket(n) |
Equal-quantile bucketing (1..n) | factor.cs.bucket(5) |
zscore() |
Standardization (mean 0, std 1) | factor.cs.zscore() |
demean() |
Subtract cross-sectional mean | factor.cs.demean() |
Usage Example
from finlab import data
roe = data.get('fundamental_features:股東權益報酬率')
# Factor preprocessing: winsorize + standardize
clean_roe = roe.cs.winsorize(0.05, 0.95).cs.zscore()
# Bucket into 5 groups for backtesting
group = roe.cs.bucket(5)
long_group = group == 5 # Long the top quintile
# Percentile rank stock selection
position = roe.cs.rank() > 0.8 # Select top 20%
Sector-Relative Transforms & Aggregation df.sector
The df.sector accessor provides two categories of functionality:
- Within-sector transforms: Same 5 methods as
df.cs(rank / winsorize / bucket / zscore / demean), computed within each sector - Within-sector aggregation: mean / std / median / sum / min / max / count — sector-level statistics
Transform Methods
| Method | Description | Example |
|---|---|---|
rank() |
Within-sector percentile rank | factor.sector.rank() |
winsorize(lower, upper) |
Within-sector winsorization | factor.sector.winsorize(0.05, 0.95) |
bucket(n) |
Within-sector bucketing | factor.sector.bucket(3) |
zscore() |
Within-sector standardization | factor.sector.zscore() |
demean() |
Within-sector demeaning | factor.sector.demean() |
Aggregation Methods
| Method | Description | Example |
|---|---|---|
mean() |
Sector mean | roe.sector.mean() |
std(ddof=1) |
Sector standard deviation | roe.sector.std() |
median() |
Sector median | roe.sector.median() |
sum() |
Sector sum | revenue.sector.sum() |
min() |
Sector minimum | close.sector.min() |
max() |
Sector maximum | close.sector.max() |
count() |
Count of non-NaN values in sector | roe.sector.count() |
Usage Example
from finlab import data
roe = data.get('fundamental_features:股東權益報酬率')
close = data.get('price:收盤價')
# Within-sector rank (top 20% ROE within each sector)
position = roe.sector.rank() > 0.8
# Within-sector standardization (removes sector bias)
neutralized_roe = roe.sector.zscore()
# Compare against sector average
sector_avg_roe = roe.sector.mean()
above_avg = roe > sector_avg_roe # Stocks above sector average
sector vs neutralize_industry()
df.sector.zscore()achieves the same effect asneutralize_industry()(industry neutralization)df.sectoroffers more flexibility: ranking, bucketing, aggregation, and more
Complete Example: Multi-Filter Strategy
from finlab import data
from finlab.backtest import sim
# Step 1: Load data
close = data.get('price:收盤價')
marketcap = data.get('etl:market_value')
revenue = data.get('monthly_revenue:當月營收')
roe = data.get('fundamental_features:股東權益報酬率')
# Step 2: Define filter conditions
cond1 = marketcap.is_smallest(100) # Small-cap
cond2 = (revenue.average(3) / revenue.average(12)) > 1.2 # Revenue growth
cond3 = roe.rank(pct=True, axis=1) > 0.7 # Top 30% ROE
cond4 = (close > close.average(20)).sustain(3) # Above 20MA for 3 days
# Step 3: Combine conditions
position = cond1 & cond2 & cond3 & cond4
# Step 4: Limit number of holdings
position = position.is_largest(20) # Max 20 stocks
# Step 5: Backtest with stop-loss/take-profit
report = sim(position, resample='M', stop_loss=0.1, take_profit=0.2)
report.display()
# Step 6: Analyze performance
metrics = report.get_metrics()
print(f"Annual return: {metrics['annual_return']:.2%}")
print(f"Sharpe ratio: {metrics['daily_sharpe']:.2f}")
print(f"Max drawdown: {metrics['max_drawdown']:.2%}")
FAQ
Q: How does FinlabDataFrame automatically align different frequency data?
from finlab import data
# Daily data
close = data.get('price:收盤價') # Updated daily
print(close.index[0]) # 2010-01-04
# Monthly data
revenue = data.get(
'monthly_revenue:當月營收'
) # Published around the 10th of each month
print(revenue.index[0]) # 2010-01-10
# Direct operations auto-align (revenue is forward-filled to daily frequency)
position = (close > 100) & (revenue > 1e8)
# position is daily data, revenue has been auto-filled
Alignment Rules: - Low frequency to high frequency: Forward fill - Quarterly reports (financial statements): Automatically delayed to the financial statement announcement date
Q: Why does a plain pandas DataFrame on the left give a different result?
The left operand decides the alignment. With a plain pd.DataFrame on the left, + - * / // % ** & | ^ use pandas rules: no forward fill of dates, union of stocks, and a plain DataFrame result (comparisons > < == are not affected). When the dates or stocks differ, finlab emits a PandasLeftOperandWarning.
mask = pd.DataFrame(True, index=close.index[:1], columns=close.columns)
mask & (close > 0) # pandas alignment: True on the first date only, warns
(close > 0) & FinlabDataFrame(
mask
) # FinlabDataFrame alignment: forward-filled to every date
Put the FinlabDataFrame on the left, or wrap the other side with FinlabDataFrame(df) (from finlab.dataframe import FinlabDataFrame).
Q: How do I chain multiple FinlabDataFrame methods?
from finlab import data
close = data.get('price:收盤價')
# Method chaining (similar to pandas method chaining)
position = (
close.average(20) # Compute 20MA
.rank(pct=True, axis=1) # Rank
.is_largest(30) # Select top 30
)
# Equivalent to
ma20 = close.average(20)
ranked = ma20.rank(pct=True, axis=1)
position = ranked.is_largest(30)
Q: What is the difference between is_largest() and rank() + threshold?
from finlab import data
close = data.get('price:收盤價')
# Method 1: is_largest (fixed count)
position1 = close.is_largest(30) # Always selects 30 stocks
# Method 2: rank + threshold (fixed percentage)
position2 = close.rank(pct=True, axis=1) > 0.8 # Selects top 20% (count varies)
# Difference:
# - Method 1: Fixed number of holdings (suitable for fixed capital allocation)
# - Method 2: Variable number of holdings (adapts to market conditions)
Q: How does trailing stop (trail_stop) work?
For a regular long position with trade_at_price="close" and touched_exit=False, the drawdown from the peak is measured against entry cost, not against the peak: an exit signal fires when (holding-period peak price - current price) / entry cost > trail_stop. The comparison is strict, subject to floating-point rounding at the boundary.
This is a signal threshold, not a guaranteed fill price: execution dates and prices still follow the sim() execution settings. touched_exit=True uses intraday highs/lows and touch rules instead; retain_cost_when_rebalance controls whether the entry cost and peak reset at each rebalance.
Trailing stops can be handled by the backtest engine with sim(trail_stop=...), or converted into exit signals while building positions with hold_until(trail_stop=...). Use trail_stop_activation when the trailing stop should activate only after a configured profit threshold is reached.
import pandas as pd
from finlab import data
from finlab.backtest import sim
close = data.get('price:收盤價')
position = close > close.average(20)
# Example: Entry price is 100
# - stop_loss=0.1: Exit if price drops below 90
# - take_profit=0.2: Exit when price reaches 120 (fixed take-profit)
# - trail_stop=0.2: Signal exit once price falls more than 20% of the entry price below the peak
# Scenario: Entry at 100 -> rises to 150 -> drops to 120
# - take_profit=0.2: Exits at 120, does not wait for 150
# - trail_stop=0.2: Threshold = 150 - 0.2 * 100 = 130 (not 120); signal fires once price falls below 130
# Fixed take-profit
report_fixed = sim(position, resample='M', stop_loss=0.1, take_profit=0.2)
# Trailing stop
report_trailing = sim(position, resample='M', stop_loss=0.1, trail_stop=0.2)
# Generate trailing-stop exits in hold_until first
position_trailing = position.hold_until(
exit=pd.DataFrame(False, index=position.index, columns=position.columns),
trail_stop=0.2,
trail_stop_activation=0.3,
)
Q: How do I use pandas methods on FinlabDataFrame?
from finlab import data
close = data.get('price:收盤價')
# FinlabDataFrame is fully compatible with pandas
close.rolling(20).mean() # pandas method
close.fillna(0) # pandas method
close.dropna() # pandas method
# All pandas methods work
# Results remain FinlabDataFrame (retaining extended features)
Resources
- Stock Selection Tutorial - FinlabDataFrame practical applications
- Backtesting Tutorial - Complete backtesting workflow
- Technical Indicator Selection Examples - Strategy examples
- pandas Official Documentation - pandas fundamentals
Batch industry neutralization and alignment profiling
neutralize_industry_batch() accepts a factor list or mapping. Factors with
identical dates and symbols share category mapping, date conversion, and
vectorized industry passes; the output preserves the input container shape.
from finlab.dataframe import neutralize_industry_batch, factor_alignment_profile
neutral = neutralize_industry_batch({"value": value, "quality": quality})
with factor_alignment_profile() as profile:
combined = daily_factor + quarterly_factor
print(profile.to_dict())
Default categories use a cache tied to the active market and data lifecycle.
Changing storage, forcing a categories refresh, and data.clear() invalidate
it. You can also call data.clear_category_cache() explicitly and inspect raw
and mapping hits with data.category_cache_stats().
Args:
n (positive-int): 設定比較前第n筆低。
Returns:
(pd.DataFrame): data
Examples:
收盤價是否低於10日前股價