Skip to content

Getting Started with Strategy Development (10-Minute Guide)

Want to quickly learn the essential features of FinLab's strategy development toolkit? This guide covers everything you need to go from data to backtest results.

1. Import Packages

from finlab import data
from finlab.backtest import sim
from finlab.market import USMarket

2. Get Data

2-1. Time Series Data

Use data.get() to retrieve various time series datasets. The returned DataFrame has dates as the index and stock symbols as columns.

For the US market, first call data.set_market('us') to switch the data source.

from finlab import data
from finlab.market 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 data tables include:

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:netIncome Quarterly net income
us_balance_sheet:totalAssets Total assets
us_cash_flow:operatingCashFlow Operating cash flow
us_key_metrics:peRatio Price-to-earnings ratio
us_key_metrics:pbRatio Price-to-book ratio
us_key_metrics:roe Return on equity
us_key_metrics:marketCap Market capitalization

2-2. Non-Time-Series Data

Some datasets (e.g., company profile information) are not time series. They are static tables with one row per company.

profile = data.get('us_company_profile')
profile.head(3)

3. Write a Strategy

3-1. Strategy Conditions

Entry: Price crosses above the 20-day moving average. Exit: Price crosses below the 60-day moving average. Portfolio: Hold at most 10 stocks. When more than 10 entry signals fire simultaneously, prioritize stocks with lower P/B ratios.

3-2. The hold_until Function

This is arguably the most important syntactic sugar in FinLab strategy development. Given a DataFrame of entries (boolean entry signals) and a DataFrame of exits (boolean exit signals), entries.hold_until(exits) means: when the entry signal is True, buy and hold the stock until the exit signal becomes True, then sell.

This function 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, you can provide a custom ranking to determine priority. You can also add stop-loss and take-profit rules to create additional exit triggers.

hold_until Parameter Reference

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
from finlab import data
from finlab.backtest import sim
from finlab.market import USMarket

data.set_market('us')

close = data.get('price:adj_close')
close = close[close.index.dayofweek < 5]  # remove weekends

pb = data.get('us_key_metrics:pbRatio')

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)

4. Backtesting

4-1. Display Backtest Results

backtest_report = sim(position, market=USMarket(), fee_ratio=0.001, tax_ratio=0)
backtest_report.display()

4-2. Get Cumulative Return Series

The creturn attribute returns a time series of cumulative returns, useful for custom plotting or further analysis.

backtest_report.creturn

4-3. Get Current Holdings and Expected Rebalancing

position_info() shows the most recent portfolio positions and any expected changes at the next rebalancing date.

backtest_report.position_info()

4-4. Get Trade Records

get_trades() returns a DataFrame containing entry/exit details for every completed trade. 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
trade_record = backtest_report.get_trades()
trade_record.head(5)

4-5. Trade Record Application: Return Distribution

Using the trade records, you can plot the distribution of trade returns to visualize 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)

4-6. Liquidity Risk Analysis

Use LiquidityAnalysis to assess whether your strategy's trades are realistic given actual market liquidity. It checks what fraction of trades exceed specified volume and turnover thresholds, helping you gauge the strategy's capacity.

from finlab.analysis.liquidityAnalysis import LiquidityAnalysis

backtest_report.run_analysis(LiquidityAnalysis(required_volume=100000, required_turnover=1000000))

4-7. Get Strategy Statistics

Retrieve a comprehensive set of performance metrics including Sharpe ratio, Sortino ratio, maximum drawdown, and return statistics.

Key Metric Interpretation:

Sharpe Ratio: Below zero means 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+ represents an exceptionally strong strategy (rare and often capacity-constrained)

Kurtosis (kurt): Compared to a normal (Gaussian) distribution, higher kurtosis means the return distribution is more peaked -- typically indicating more consistent performance with occasional extreme events. For the broad market, kurtosis is roughly 1-2. A strategy with kurtosis above 2 is acceptable, and above 5 is quite good. For skewness, more negative values indicate a left tail (occasional large losses). Below -0.5 is acceptable, and around -1 is decent.

backtest_report.get_stats()

5. Summary

This guide covered the core workflow:

  1. Data: data.set_market('us') and data.get() to retrieve price and fundamental data
  2. Strategy: Boolean entry/exit signals combined with hold_until() for position management
  3. Backtest: sim() with USMarket() for realistic US market simulation
  4. Analysis: Cumulative returns, trade records, return distributions, liquidity checks, and performance statistics

Each of these components is modular -- you can swap in different data sources, entry/exit logic, or analysis methods without changing the rest of the pipeline.