Get started
Your first strategy in 10 minutes
On this page4
This lesson takes you through FinLab's essential strategy tools, from data to backtest results, in about ten minutes. You will write one complete US strategy with entry and exit rules, backtest it, then dig into its trades and statistics.
- 1 Get data
data.get('price:adj_close')Dates down, stocks across
- 2 Write a condition
close > close.average(60)True = hold that day
- 3 Backtest
sim(position, resample='M')Returns, drawdown, trades
- 4 Go live
PortfolioSyncManagerUpdates and orders every day
Build and backtest the strategy
Import the packages
from finlab import data
from finlab.backtest import sim
from finlab.markets.us import USMarketLoad time-series data
data.get() loads a time-series dataset as a DataFrame with dates as the index and stock symbols as the columns. For the US market, first call data.set_market('us') to switch the data source.
from finlab import data
from finlab.markets.us import USMarket
data.set_market('us')
close = data.get('price:adj_close')
close = close[close.index.dayofweek < 5] # remove weekends
close.iloc[:5, :5]Common US market datasets:
| Dataset | Description |
|---|---|
price:adj_close | Split- and dividend-adjusted closing price |
price:volume | Daily trading volume |
us_income_statement:revenue | Quarterly revenue |
us_income_statement:net_income | Quarterly net income |
us_income_statement:eps_diluted | Quarterly diluted EPS |
us_balance_sheet:total_assets | Total assets |
us_balance_sheet:total_stockholders_equity | Stockholders' equity |
us_cash_flow:operating_cash_flow | Operating cash flow |
etl:us_market_value | Daily market capitalization |
Quarterly statements are aligned to each SEC filing date, the day they became public. Compute valuation ratios from them for backtests: for example, P/E as price / eps.rolling(4).sum() and P/B as market_value / equity. The us_ratios:* and us_key_metrics:* tables hold only recent daily snapshots, so use them to screen on the latest values, not as historical factors.
Load non-time-series data
Some datasets, such as company profiles, are not time series: they are static tables with one row per company.
profile = data.get('us_company_profile')
profile.head(3)Decide the rules
The strategy in this lesson:
- Entry: the price crosses above its 20-day moving average.
- Exit: the price crosses below its 60-day moving average.
- Portfolio: hold at most 10 stocks. When more than 10 entry signals fire at once, prefer the stocks with lower P/B ratios.
Hold each stock until its exit signal
hold_until is arguably the most important shortcut in FinLab strategy development. Given a boolean DataFrame of entries and one of exits, entries.hold_until(exits) means: when the entry signal is True, buy and hold the stock until the exit signal becomes True, then sell.
It has many fine-grained settings. You can limit the portfolio to at most N stocks with rotation; when more than N entry signals fire on the same day, a custom ranking decides which come first; and stop-loss and take-profit rules add more ways to exit.
| Parameter | Type | Description |
|---|---|---|
exit | pd.DataFrame | Exit signal (boolean DataFrame, same shape as entries) |
nstocks_limit | int | Maximum number of stocks to hold simultaneously |
stop_loss | float | Stop-loss threshold. Example: 0.1 means exit when the price drops 10% from the entry cost |
take_profit | float | Take-profit threshold. Example: 0.1 means exit when the price rises 10% from the entry cost |
trade_at | str | Reference price for stop-loss/take-profit. Options: 'close' or 'open' |
rank | pd.DataFrame | When the number of entry signals exceeds nstocks_limit, stocks with higher rank values are selected first |
The complete strategy:
from finlab import data
from finlab.backtest import sim
from finlab.markets.us import USMarket
data.set_market('us')
close = data.get('price:adj_close')
close = close[close.index.dayofweek < 5] # remove weekends
# Price-to-book: market value / stockholders' equity
pb = data.get('etl:us_market_value') / data.get(
'us_balance_sheet:total_stockholders_equity'
)
pb = pb[pb > 0]
sma20 = close.average(20)
sma60 = close.average(60)
entries = close > sma20
exits = close < sma60
position = entries.hold_until(exits, nstocks_limit=10, rank=-pb, take_profit=0.4)
backtest_report = sim(position, market=USMarket(), fee_ratio=0.001, tax_ratio=0)Display the backtest
backtest_report = sim(position, market=USMarket(), fee_ratio=0.001, tax_ratio=0)
backtest_report.display()Dig into the results
Each snippet below uses backtest_report from the steps above.
Cumulative return series
The creturn attribute is a time series of cumulative returns, useful for custom plots or further analysis.
backtest_report.creturnCurrent holdings and the next rebalance
position_info() shows the latest portfolio positions and any changes expected at the next rebalancing date.
backtest_report.position_info()Trade records
get_trades() returns a DataFrame with the entry and exit details of every completed trade.
trade_record = backtest_report.get_trades()
trade_record.head(5)Key columns:
| Column | Description |
|---|---|
entry_sig_date | Date the entry signal was generated |
exit_sig_date | Date the exit signal was generated |
entry_date | Actual entry (execution) date |
exit_date | Actual exit (execution) date |
period | Holding period in days |
position | Position weight (fraction of portfolio) |
return | Trade return (percentage) |
mdd | Maximum drawdown during the holding period |
mae | Maximum Adverse Excursion: the worst unrealized loss during the trade |
gmfe | Maximum Favorable Excursion: the best unrealized gain during the trade |
bmfe | Maximum Favorable Excursion before the MAE occurred |
Return distribution
From the trade records, plot the distribution of trade returns to see the balance between winning and losing trades.
import plotly.express as px
trade_record['profit_loss'] = trade_record['return'].apply(
lambda s: 'profit' if s > 0 else 'loss'
)
win_ratio = round(
sum(trade_record['profit_loss'] == 'profit')
/ len(trade_record['profit_loss'])
* 100,
2,
)
return_mean = round(trade_record['return'].mean(), 2)
fig = px.histogram(
trade_record,
x="return",
color="profit_loss",
title=f'profit_loss_hist - win_ratio: {win_ratio} %',
)
fig.add_vline(
x=return_mean,
line_width=3,
line_dash="dash",
line_color="green",
annotation_position="top left",
annotation_text=f'avg_return:{return_mean}',
row=1,
col=1,
)Liquidity risk
LiquidityAnalysis checks whether the strategy's trades are realistic given actual market liquidity. It reports what fraction of trades exceed the volume and turnover thresholds you set, which helps you gauge the strategy's capacity.
from finlab.analysis.liquidityAnalysis import LiquidityAnalysis
backtest_report.run_analysis(
LiquidityAnalysis(required_volume=100000, required_turnover=1000000)
)Strategy statistics
get_stats() returns a full set of performance metrics, including the Sharpe ratio, Sortino ratio, maximum drawdown and return statistics.
backtest_report.get_stats()How to read the key metrics:
- Sharpe ratio: below zero, the strategy loses money on a risk-adjusted basis. For a typical stock selection strategy:
- 0.7 is achievable for most discretionary swing traders;
- 0.9 is solid for an ETF-like systematic strategy;
- 1.3+ is the target for a well-designed quantitative strategy;
- 2.0+ is an exceptionally strong strategy (rare, and often capacity-constrained).
- Kurtosis (
kurt): compared with a normal (Gaussian) distribution, higher kurtosis means a more peaked return distribution, typically more consistent performance with occasional extreme events. For the broad market, kurtosis is roughly 1–2. Above 2 is acceptable for a strategy, and above 5 is quite good. - Skewness: more negative values mean a left tail (occasional large losses). Below -0.5 is acceptable, and around -1 is decent.
What you covered
- Data:
data.set_market('us')anddata.get()load price and fundamental data. - Strategy: boolean entry and exit signals, combined with
hold_until()to manage positions. - Backtest:
sim()withUSMarket()simulates the US market realistically. - Analysis: cumulative returns, trade records, return distributions, liquidity checks and performance statistics.
Each part is modular: you can swap in different data, entry and exit logic, or analysis methods without changing the rest of the pipeline.
Next steps
- Pandas for strategies The Pandas operations behind every rule.
- Write better rules Ranking, combining conditions, entries and exits.
- Backtesting Rebalancing, stops, fees and the trade log in
sim(). - 52-week high momentum A complete strategy example to adapt.
Was this page helpful?