Skip to content

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:

imageconds

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.

is-largest

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)

Screen-Shot-2021-10-26-at-6-35-05-AM

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)

rise(n) - Value is Rising

Check whether the current value is higher than the value n periods ago.

Screen-Shot-2021-10-26-at-6-43-41-AM

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.

from finlab import data

data.set_market('us')
position = data.get('price:adj_close').is_largest(10)
exit_signals = position.is_exit()