Computing Stock Screening Conditions
FinlabDataFrame is FinLab's stock screening tool built on top of Pandas DataFrame, letting you express complex screening strategies concisely. New to Pandas? See 10 Minutes to Pandas.
Quick Reference
| Category | Function | Description |
|---|---|---|
| Basic Operations | +, -, *, /, >, <, &, \|, ~ |
Arithmetic, comparison, logical operations |
| Screening | is_largest(n) |
Select the top n stocks by value |
is_smallest(n) |
Select the bottom n stocks by value | |
average(n) |
Compute n-period moving average | |
| Entry/Exit | hold_until(exit) |
Set entry/exit signals |
is_entry() |
Extract entry points | |
is_exit() |
Extract exit points | |
| Trend | rise(n) |
Whether value is higher than n periods ago |
fall(n) |
Whether value is lower than n periods ago | |
sustain(window, count) |
Condition sustained for multiple periods | |
| Neutralization | neutralize(factors) |
Factor neutralization (regression residuals) |
neutralize_industry() |
Industry neutralization | |
| Other | quantile_row(q) |
Cross-sectional quantile |
groupby_category() |
Group by industry (Taiwan market only) | |
index_str_to_date() |
Convert index format |
Differences from Pandas DataFrame
| Difference | Description |
|---|---|
| Additional Functions | Provides screening-specific functions like is_largest(), sustain(), hold_until(), etc. |
| Auto-alignment | Automatically aligns data of different frequencies (daily, monthly, quarterly) before operations, no manual reindex needed |
Creating a FinlabDataFrame
Method 1: Using finlab.data.get()
Data obtained via finlab.data.get() is automatically in FinlabDataFrame format.
Method 2: Converting a Pandas DataFrame
import pandas as pd
from finlab.dataframe import FinlabDataFrame
df = pd.DataFrame()
df_finlab = FinlabDataFrame(df)
Operators
Similar to pd.DataFrame, operations are performed element-wise at corresponding positions. The following operators are supported:
| Type | Operator | Description |
|---|---|---|
| Arithmetic | + |
Addition |
- |
Subtraction | |
* |
Multiplication | |
/ |
Division | |
| Comparison | > |
Greater than |
>= |
Greater than or equal | |
== |
Equal | |
< |
Less than | |
<= |
Less than or equal | |
| Logical | & |
AND |
\| |
OR | |
~ |
NOT |
Example: Select stocks with closing price between 10 and 100
from finlab import data
data.set_market('us')
close = data.get('price:adj_close')
# Closing price between $10 and $100
between_10_100 = (close > 10) & (close < 100)
Automatic frequency alignment
When operating on data of different frequencies (daily, monthly, quarterly), FinlabDataFrame handles alignment automatically:
| Alignment Step | Method |
|---|---|
| Dates (index) | Union of dates; missing values filled with the most recent data (forward fill) |
| Stocks (columns) | Intersection; stocks without corresponding data are excluded |
Example: Combining daily stock prices with quarterly financial data
from finlab import data
data.set_market('us')
# Get FinlabDataFrame
close = data.get('price:adj_close') # daily
net_income = data.get('us_income_statement:net_income') # quarterly
total_assets = data.get('us_balance_sheet:total_assets') # quarterly
roa = net_income / total_assets[total_assets > 0]
# Intersection of two screening conditions
cond1 = close > 50
cond2 = roa > 0
cond_1_2 = cond1 & cond2
Illustration for a single stock: cond1 changes daily, cond2 only when a new quarterly report is filed, and cond_1_2 follows both:

Why does the combined data have fewer rows?
Although dates are unioned, the actual data only retains the overlapping time range. For example: if cond1 starts from 2010 and cond2 starts from 2013, the combined data starts from 2013.
Screening Functions
average(n) - Moving Average
Compute the n-period moving average.
from finlab import data
data.set_market('us')
close = data.get('price:adj_close')
sma = close.average(10)
cond = close > sma # Price above 10-day moving average
A stock gets NaN when fewer than half of the values in the window are available. Visual inspection of one stock:
import matplotlib.pyplot as plt
ax = close.loc['2025', 'AAPL'].plot(label='close')
sma.loc['2025', 'AAPL'].plot(ax=ax, label='sma')
cond.loc['2025', 'AAPL'].astype(int).plot(ax=ax, secondary_y=True, label='cond')
ax.legend(loc='upper left')
plt.show()
is_largest(n) - Top n Stocks by Value
Select the top n stocks by value each day.

from finlab import data
data.set_market('us')
net_income = data.get('us_income_statement:net_income')
total_assets = data.get('us_balance_sheet:total_assets')
roa = net_income / total_assets[total_assets > 0]
good_stocks = roa.is_largest(10) # Top 10 ROA stocks on each date
The result is a boolean DataFrame with one column per stock, True for the 10 selected stocks. Quarterly data is converted to filing dates first, so the rows are the dates on which reports became public and each date only compares values already disclosed.
is_smallest(n) - Bottom n Stocks by Value
Select the bottom n stocks by value each day.
from finlab import data
data.set_market('us')
marketcap = data.get('etl:us_market_value')
equity = data.get('us_balance_sheet:total_stockholders_equity')
pb = marketcap / equity[equity > 0] # price-to-book, positive equity only
cheap_stocks = pb.is_smallest(10) # Bottom 10 by P/B ratio
Entry/Exit Signals (hold_until)
Set entry/exit logic:
from finlab import data
data.set_market('us')
close = data.get('price:adj_close')
buy = close > close.average(5)
sell = close < close.average(20)
position = buy.hold_until(sell)

Advanced Parameters
| Parameter | Description | Example |
|---|---|---|
| stop_loss | Stop-loss percentage | buy.hold_until(sell, stop_loss=0.1) |
| take_profit | Take-profit percentage | buy.hold_until(sell, take_profit=0.2) |
| nstocks_limit | Maximum number of stocks held (sector rotation) | buy.hold_until(sell, nstocks_limit=5) |
| rank | Stock selection priority (positive = descending) | buy.hold_until(sell, rank=-pb) |
Complete Strategy Example
Enter when price > 20-day MA, exit when price < 60-day MA, hold at most 10 stocks, prioritize stocks with lower P/B ratio.
from finlab import data
from finlab.backtest import sim
data.set_market('us')
close = data.get('price:adj_close')
marketcap = data.get('etl:us_market_value')
equity = data.get('us_balance_sheet:total_stockholders_equity')
pb = marketcap / equity[equity > 0]
entries = close > close.average(20)
exits = close < close.average(60)
position = entries.hold_until(exits, nstocks_limit=10, rank=-pb)
sim(position)
Value Change Trends
rise(n) - Value is Rising
Check whether the current value is higher than the value n periods ago.

from finlab import data
data.set_market('us')
data.get('price:adj_close').rise(10) # Higher than 10 days ago?
fall(n) - Value is Falling
Check whether the current value is lower than the value n periods ago.
from finlab import data
data.set_market('us')
data.get('price:adj_close').fall(10) # Lower than 10 days ago?
sustain(window, count) - Condition Sustained
Check whether the condition is met at least count times within a window period.
from finlab import data
data.set_market('us')
close = data.get('price:adj_close')
close.rise().sustain(2) # Rising for 2 consecutive days
close.rise().sustain(3, 2) # Rising on 2 out of 3 days
Factor Neutralization
In multi-factor screening, factors may be correlated. For example, a cheap-valuation factor (high earnings yield, low P/E) may simply be selecting small-cap stocks. Neutralization uses cross-sectional regression to remove the influence of specified factors, leaving the pure alpha signal.
neutralize(factors) - Factor Neutralization
Perform cross-sectional regression against specified factors and return residuals (values after removing the factor's influence).
import numpy as np
from finlab import data
data.set_market('us')
close = data.get('price:adj_close')
raw_close = data.get('price:close') # unadjusted price for valuation
eps = data.get('us_income_statement:eps_diluted')
marketcap = data.get('etl:us_market_value')
factor = eps.rolling(4).sum() / raw_close # trailing-12-month earnings yield
size = np.log(marketcap).replace(
[np.inf, -np.inf], np.nan
) # log market cap, finite values only
# Remove market cap influence
neutralized = factor.neutralize(size)
Supports neutralizing against multiple factors simultaneously:
volatility = (close / close.shift(1) - 1).rolling(60).std() # 60-day volatility
# Pass multiple factors as a list
neutralized = factor.neutralize([size, volatility])
# Or use a dict for naming (convenient for debugging)
neutralized = factor.neutralize(
{
'size': size,
'size2': size**2,
'volatility': volatility,
}
)
neutralize_industry() - Industry Neutralization
Perform cross-sectional regression using industry dummy variables and return residuals. Pass the classification as categories, a DataFrame with symbol and category columns; without it, the Taiwan security_categories classification is used. For U.S. stocks, use the sectors in us_company_profile:
from finlab import data
profile = data.get('us_company_profile')
sectors = profile[['symbol', 'sector']].rename(columns={'sector': 'category'})
# Remove sector effects
neutralized = factor.neutralize_industry(categories=sectors)
You can also use your own classification:
import pandas as pd
custom_cats = pd.DataFrame(
{'symbol': ['AAPL', 'MSFT', 'JPM'], 'category': ['Hardware', 'Software', 'Banks']}
)
neutralized = factor.neutralize_industry(categories=custom_cats)
Tip
Neutralized factors can be directly used with rank(), is_largest(), and other screening functions. The selected stocks will better reflect the factor's own effect rather than being dominated by market cap or industry.
Other Useful Functions
quantile_row(q) - Cross-sectional Quantile
Get the quantile value across stocks for each day.
from finlab import data
data.set_market('us')
data.get('price:adj_close').quantile_row(0.9) # 90th percentile of stock prices daily
index_str_to_date() - Index Format Conversion
Convert a quarterly report string index to the dates on which the reports were filed. Each company's value takes effect on its own filing date.
| Before | After |
|---|---|
| 2024-US-Q3 (U.S. quarterly report) | Each company's SEC filing date, e.g. 2024-11-01 for AAPL |
from finlab import data
data.set_market('us')
data.get('us_income_statement:revenue').index_str_to_date()
data.get('us_balance_sheet:cash_and_cash_equivalents').index_str_to_date()
You rarely need to call it yourself: operators, is_largest(), rank(axis=1) and the other cross-sectional methods convert quarterly data automatically.
groupby_category() - Group by Industry
Group data by industry, similar to pd.DataFrame.groupby(). Taiwan market only: it uses the Taiwan industry classification (security_categories).
For U.S. sector statistics, use the sector accessor with a symbol-to-sector mapping. Each stock receives its sector's statistic:
from finlab import data
data.set_market('us')
marketcap = data.get('etl:us_market_value')
equity = data.get('us_balance_sheet:total_stockholders_equity')
pb = marketcap / equity[equity > 0]
profile = data.get('us_company_profile')
sectors = dict(zip(profile['symbol'], profile['sector']))
sector_pb = pb.sector(sectors).median()
sector_pb['AAPL'].plot() # Median P/B ratio of AAPL's sector (Technology)
is_entry() - Extract Entry Points
Extract entry time points from position signals.
from finlab import data
data.set_market('us')
position = data.get('price:adj_close').is_largest(10)
entry_signals = position.is_entry()
is_exit() - Extract Exit Points
Extract exit time points from position signals.