Docs
Open Studio

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. 1 Get data
    data.get('price:adj_close')

    Dates down, stocks across

  2. 2 Write a condition
    close > close.average(60)

    True = hold that day

  3. 3 Backtest
    sim(position, resample='M')

    Returns, drawdown, trades

  4. 4 Go live
    PortfolioSyncManager

    Updates 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 USMarket

Load 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:

DatasetDescription
price:adj_closeSplit- and dividend-adjusted closing price
price:volumeDaily trading volume
us_income_statement:revenueQuarterly revenue
us_income_statement:net_incomeQuarterly net income
us_income_statement:eps_dilutedQuarterly diluted EPS
us_balance_sheet:total_assetsTotal assets
us_balance_sheet:total_stockholders_equityStockholders' equity
us_cash_flow:operating_cash_flowOperating cash flow
etl:us_market_valueDaily 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.

ParameterTypeDescription
exitpd.DataFrameExit signal (boolean DataFrame, same shape as entries)
nstocks_limitintMaximum number of stocks to hold simultaneously
stop_lossfloatStop-loss threshold. Example: 0.1 means exit when the price drops 10% from the entry cost
take_profitfloatTake-profit threshold. Example: 0.1 means exit when the price rises 10% from the entry cost
trade_atstrReference price for stop-loss/take-profit. Options: 'close' or 'open'
rankpd.DataFrameWhen the number of entry signals exceeds nstocks_limit, stocks with higher rank values are selected first

The complete strategy:

strategy.py
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.creturn

Current 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:

ColumnDescription
entry_sig_dateDate the entry signal was generated
exit_sig_dateDate the exit signal was generated
entry_dateActual entry (execution) date
exit_dateActual exit (execution) date
periodHolding period in days
positionPosition weight (fraction of portfolio)
returnTrade return (percentage)
mddMaximum drawdown during the holding period
maeMaximum Adverse Excursion: the worst unrealized loss during the trade
gmfeMaximum Favorable Excursion: the best unrealized gain during the trade
bmfeMaximum 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

  1. Data: data.set_market('us') and data.get() load price and fundamental data.
  2. Strategy: boolean entry and exit signals, combined with hold_until() to manage positions.
  3. Backtest: sim() with USMarket() simulates the US market realistically.
  4. 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

↑↓ Move Enter Open Esc Close