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
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.
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.
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.
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 |
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.
5. Summary
This guide covered the core workflow:
- Data:
data.set_market('us')anddata.get()to retrieve price and fundamental data - Strategy: Boolean entry/exit signals combined with
hold_until()for position management - Backtest:
sim()withUSMarket()for realistic US market simulation - 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.