Docs
Open Studio

Workflows

From Research to Live Trading

On this page12

This page takes one "revenue growth + technical breakout" strategy for S&P 500 stocks through the whole life of a quantitative strategy: idea, backtest, optimization and validation, all the way to uploading it, trading it live and tracking its performance.

By the end you will have a strategy whose conditions, stops and out-of-sample results have been checked; it is uploaded to FinLab, sized to your capital in shares, and ready to trade through a broker account and review on a schedule.

Before you start, you need:

An AI coding assistant can help at every stage: after installing FinLab Skill, it can write code, interpret performance and troubleshoot errors from idea to live deployment.

The whole workflow looks like this. If validation fails, or live results call for changes, go back to strategy development:

Yes

No

Yes

No

Strategy Idea

Data Exploration

Strategy Development

Strategy Optimization

In-Depth Analysis

Out-of-Sample Testing

Validation Passed?

Pre-Deployment Prep

Live Trading

Performance Tracking

Adjustment Needed?

1. Write down the hypothesis and check the data

The strategy hypothesis:

  • Fundamentals: Quarterly revenue is growing more than 10% year over year
  • Technicals: Price breaks above moving averages, confirming momentum
  • Risk Control: Exit when price drops below moving average, set 10% stop loss

First, check whether the required data covers enough history and how much is missing:

from finlab import data
import pandas as pd

# Load required data for S&P 500 stocks
data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')
    rev = data.get('us_income_statement:revenue')

# Check data range (revenue is quarterly, indexed by labels such as 2024-US-Q3)
print(f"Close-price range: {close.index[0]} ~ {close.index[-1]}")
print(f"Revenue range:     {rev.index[0]} ~ {rev.index[-1]}")

# Check missing value ratio
print(f"Close-price missing: {close.isna().sum().sum() / close.size * 100:.2f}%")
print(f"Revenue missing:     {rev.isna().sum().sum() / rev.size * 100:.2f}%")

2. Write the strategy and backtest it

Strategy logic

from finlab import data
from finlab.backtest import sim

# 1. Load data
data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')
    rev = data.get('us_income_statement:revenue')

# 2. Calculate technical indicators
ma20 = close.average(20)
ma60 = close.average(60)

# 3. Calculate revenue indicators (quarterly, aligned to each SEC filing date)
rev_yoy = rev.pct_change(4)  # Year-over-year revenue growth

# 4. Define entry conditions
entry_tech = (close > ma20) & (
    close > ma60
)  # Technicals: breakout above moving averages
entry_fund = rev_yoy > 0.1  # Fundamentals: revenue growth above 10%

entry_signal = entry_tech & entry_fund

# 5. Define exit conditions
exit_signal = close < ma20  # Break below 20-day MA

# 6. Combine entry/exit signals (with stop loss)
position = entry_signal.hold_until(
    exit=exit_signal,
    stop_loss=0.1,  # Stop loss 10%
)

# 7. Backtest
report = sim(
    position,
    resample='M',  # Monthly rebalance
    position_limit=0.1,  # Single-holding cap 10%
    upload=False,
    name="Revenue Growth Breakout v1.0",
)

# 8. Display performance
report.display()

First look at performance

stats = report.get_stats()

print(f"Annualized return: {stats['cagr'] * 100:.2f}%")
print(f"Sharpe ratio:      {stats['daily_sharpe']:.2f}")
print(f"Max drawdown:      {stats['max_drawdown'] * 100:.2f}%")
print(f"Win rate:          {stats['win_ratio'] * 100:.2f}%")

Compare the annualized return and maximum drawdown with simply holding the S&P 500 over the same period. A Sharpe ratio well below 1 means the return does not yet compensate for the volatility; the next step is to optimize the strategy to improve risk-adjusted returns.

3. Optimize the conditions and stops

Add candidate conditions and test every combination

Add three more candidate conditions, then let sim_conditions() backtest every combination to find the best intersection. For more on combinations, see Strategy optimization.

from finlab.optimize.combinations import sim_conditions

# Original conditions
c1 = (close > ma20) & (close > ma60)  # MA bullish
c2 = rev_yoy > 0.1  # Revenue growth

# New conditions
with data.us_universe(index='SP500'):
    eps = data.get('us_income_statement:eps_diluted')
raw_close = data.get('price:close')  # unadjusted price for valuation
pe = raw_close / eps.rolling(4).sum()  # P/E on trailing-12-month diluted EPS
c3 = (pe > 0) & (pe < 20)  # Low P/E
c4 = close == close.rolling(20).max()  # New 20-day high
c5 = rev / rev.shift(1) > 0.9  # Revenue QoQ change > -10%

# Conditions dictionary
conditions = {
    'c1': c1,  # MA bullish
    'c2': c2,  # Revenue growth
    'c3': c3,  # Low P/E
    'c4': c4,  # New high
    'c5': c5,  # Revenue QoQ
}

# Test all combinations (31 total)
report_collection = sim_conditions(
    conditions=conditions,
    hold_until={'exit': exit_signal, 'stop_loss': 0.1},
    resample='M',
    position_limit=0.1,
    upload=False,
)

# Visualize comparison
report_collection.plot_stats('heatmap')

Find the best combinations

# Get performance metrics table
stats_df = report_collection.get_stats()

# Find top 3 combinations
top3 = stats_df.T.sort_values('daily_sharpe', ascending=False).head(3)
print(top3[['cagr', 'daily_sharpe', 'max_drawdown', 'win_ratio']])

The rest of this walkthrough adopts c1 & c2 & c3 (MA bullish + revenue growth + low P/E). Use whichever combination ranks first in your own run, and prefer combinations whose neighbors also rank well over a single outlier.

Adopt the best combination

# Redefine strategy using the best combination
best_entry = c1 & c2 & c3

position_optimized = best_entry.hold_until(exit=exit_signal, stop_loss=0.1)

report_opt = sim(
    position_optimized,
    resample='M',
    position_limit=0.1,
    upload=False,
    name="Revenue Growth Breakout v2.0 (optimized)",
)

Set stops from MAE/MFE

Use each trade's maximum adverse excursion (MAE) and maximum favorable excursion (GMFE) to set the stops: the stop loss is 1.2 times the 75th percentile of MAE, and the take profit is 0.8 times the 75th percentile of GMFE.

# Display volatility analysis
report_opt.display_mae_mfe_analysis()

# Get trade records
trades = report_opt.get_trades()

# Analyze MAE/MFE quantiles
mae_q75 = abs(trades['mae'].quantile(0.75))
gmfe_q75 = trades['gmfe'].quantile(0.75)

print(f"MAE Q75: {mae_q75 * 100:.2f}%")
print(f"GMFE Q75: {gmfe_q75 * 100:.2f}%")

# Adjust stop loss/take profit
position_final = best_entry.hold_until(
    exit=exit_signal,
    stop_loss=mae_q75 * 1.2,  # Stop loss = MAE Q75 * 1.2
    take_profit=gmfe_q75 * 0.8,  # Take profit = GMFE Q75 * 0.8
)

report_final = sim(
    position_final,
    resample='M',
    position_limit=0.1,
    upload=False,
    name="Revenue Growth Breakout v3.0 (final)",
)

4. Analyze liquidity, stability and excess return

For the full guide to each analysis module, see Strategy analysis modules.

Liquidity: can large capital trade it?

Require a minimum dollar volume, then rerun the backtest and compare:

with data.us_universe(index='SP500'):
    volume = data.get('price:volume')

# 20-day average dollar volume; require at least $5M traded per day
dollar_volume = (close * volume).average(20)
liquid = dollar_volume > 5_000_000

position_liquid = (best_entry & liquid).hold_until(exit=exit_signal, stop_loss=0.1)
report_liquid = sim(position_liquid, resample='M', position_limit=0.1, upload=False)

If performance collapses once illiquid stocks are excluded, the backtest relied on trades that large capital cannot execute. For Taiwan stocks, report.run_analysis('LiquidityAnalysis', required_volume=100000) also reports trades in alert, disposition and full-delivery stocks (Taiwan market only).

Stability across years

Review performance year by year and check whether some years are particularly poor:

report_final.run_analysis('PeriodStatsAnalysis')

# Output shows Sharpe ratio, return, volatility, etc. for each year
# Check if the strategy performs particularly poorly in certain years

Alpha/Beta: how much excess return?

report_final.run_analysis('AlphaBetaAnalysis')

# Displays three sections: overall summary, annual Alpha/Beta, recent Alpha/Beta
# Alpha: annualized excess return over the benchmark (S&P 500 for U.S. stocks)
# Beta: market sensitivity

To obtain raw values for further analysis:

result = report_final.run_analysis('AlphaBetaAnalysis', display=False)
print(f"Alpha: {result['overall']['alpha']:.2%}")
print(f"Beta: {result['overall']['beta']:.2f}")

A positive alpha indicates excess return that the market exposure does not explain; a beta below 1 means the strategy moves less than the market.

5. Test out of sample

Split the data into training and test sets to check that the strategy is not overfit:

# Define split date
train_end = '2022-12-31'
test_start = '2023-01-01'

# Training set backtest (through 2022)
position_train = position_final[position_final.index <= train_end]
report_train = sim(
    position_train,
    resample='M',
    position_limit=0.1,
    upload=False,
    name="Training set (through 2022)",
)

# Test set backtest (from 2023)
position_test = position_final[position_final.index >= test_start]
report_test = sim(
    position_test,
    resample='M',
    position_limit=0.1,
    upload=False,
    name="Test set (from 2023)",
)

# Performance comparison
stats_train = report_train.get_stats()
stats_test = report_test.get_stats()

comparison = pd.DataFrame(
    {
        'Training': [
            stats_train['cagr'],
            stats_train['daily_sharpe'],
            stats_train['max_drawdown'],
            stats_train['win_ratio'],
        ],
        'Testing': [
            stats_test['cagr'],
            stats_test['daily_sharpe'],
            stats_test['max_drawdown'],
            stats_test['win_ratio'],
        ],
    },
    index=['Annualized return', 'Sharpe ratio', 'Max drawdown', 'Win rate'],
)

print(comparison)

Compare training with testing; the test passes when all four hold:

MetricPass when training vs testing shows
Annualized ReturnReasonable degradation (less than 20%)
Sharpe RatioA slight decrease, still above 1
Max DrawdownA small difference
Win RateA stable value

If the test period passes these checks, the strategy is unlikely to be overfit and can proceed to deployment.

6. Upload it and plan the capital

Upload and check the target holdings

# Upload the final version to the FinLab platform
report_final.upload(name="Revenue Growth Breakout v3.0")

Then check the holdings the strategy targets:

# Get position list
position_info = report_final.position_info()
print(f"Next rebalance date: {position_info['next_trading_date']}")

# position_info keys are stock symbols (e.g., 'AAPL'), mixed with some metadata keys
# Filter out holdings (exclude metadata keys)
metadata_keys = {
    'update_date',
    'next_trading_date',
    'trade_at',
    'freq',
    'market',
    'stop_loss',
    'take_profit',
}
stocks = {k: v for k, v in position_info.items() if k not in metadata_keys}
print(f"Current holdings count: {len(stocks)}")
print("Holdings list:")
for symbol, info in stocks.items():
    print(f"  {symbol}: weight={info['weight']:.2%}, status={info['status']}")

Work out the amount per stock

# Assuming $100,000 of live capital
capital = 100_000

# Calculate the amount to invest in each stock (reusing stocks and metadata_keys from above)
for symbol, info in stocks.items():
    info['amount'] = info['weight'] * capital

print("Capital allocation:")
for symbol, info in stocks.items():
    print(f"  {symbol}: weight={info['weight']:.2%}, amount=${info['amount']:,.0f}")

7. Place live orders

Set up the broker account

This example uses Charles Schwab:

from finlab.online.schwab_account import SchwabAccount

# Reads SCHWAB_API_KEY, SCHWAB_SECRET and SCHWAB_TOKEN_PATH from the environment
account = SchwabAccount()

Taiwan brokers (SinoPac, Fubon, E.SUN, Taishin, formerly Masterlink) are supported as well; see the live trading tutorial.

Calculate target positions

from finlab.online.order_executor import Position

# Calculate target share counts
capital = 100_000  # Live capital: $100,000
target_position = Position.from_report(report_final, capital)

print(target_position)
# A list of {'stock_id', 'quantity', 'order_condition'} entries, one per stock

Place the orders

Preview first, then place the orders once they look right:

from finlab.online.order_executor import OrderExecutor

# Create order executor
order_executor = OrderExecutor(target_position, account=account)

# Order preview (view mode, no actual orders placed)
order_executor.create_orders(view_only=True)

# Execute orders (actual orders placed; test during market close for first-time use)
order_executor.create_orders()

# Update limit price (use last trade price as new limit price)
order_executor.update_order_price()

# Cancel all pending orders
order_executor.cancel_orders()

8. Track live performance

Get live positions and account value

# account: the SchwabAccount created above
# Get live positions
account_positions = account.get_position()
print(account_positions)

# Get account total value and cash
account_value = account.get_total_balance()
print(f"Account total value: {account_value:,.0f}")

Record account value every day

Recommended after each market close:

# Periodically check account status (recommended after each market close)
# account: the SchwabAccount created above
import pandas as pd

# Record daily account value
daily_value = {
    'date': pd.Timestamp.today(),
    'account_value': account.get_total_balance(),
    'positions': len(account.get_position()),
}

print(f"Date:             {daily_value['date']}")
print(f"Account value:    {daily_value['account_value']:,.0f}")
print(f"Holdings count:   {daily_value['positions']}")

# Compare with backtest
backtest_return = report_final.get_stats()['cagr']
print(f"Backtest annualized return: {backtest_return * 100:.2f}%")

Review every month

Check each month whether holdings follow the plan and whether a stop loss has been hit:

# account_positions: from the SchwabAccount above
# 1. Check if holdings follow the plan
current_stocks = set(account_positions['stock_id'])
metadata_keys = {
    'update_date',
    'next_trading_date',
    'trade_at',
    'freq',
    'market',
    'stop_loss',
    'take_profit',
}
planned_stocks = {k for k in position_info if k not in metadata_keys}

print(f"Planned holdings:  {planned_stocks}")
print(f"Actual holdings:   {current_stocks}")
print(f"Mismatches:        {planned_stocks.symmetric_difference(current_stocks)}")

# 2. Check if stop loss is triggered
for stock_id, pos in account_positions.items():
    entry_price = pos['avg_price']
    current_price = pos['current_price']
    unrealized_pnl = current_price / entry_price - 1

    if unrealized_pnl < -0.10:  # Stop loss 10%
        print(
            f"Warning: {stock_id} stop loss triggered, unrealized P&L: {unrealized_pnl * 100:.2f}%"
        )

To rebuild NAV from your real fills, dividends and cash flows and compare it with the strategy's targets day by day, see Real-trade review.

Complete code

Steps 1 through 6 in one run:

# =============================================================================
# From Research to Production: Revenue Growth Breakout Strategy Complete Example
# =============================================================================

from finlab import data
from finlab.backtest import sim
from finlab.optimize.combinations import sim_conditions

# -----------------------------------------------------------------------------
# Stages 1 & 2: Data Loading & Strategy Development
# -----------------------------------------------------------------------------

# Load data for S&P 500 stocks
data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')
    raw_close = data.get('price:close')  # unadjusted price for valuation
    rev = data.get('us_income_statement:revenue')
    eps = data.get('us_income_statement:eps_diluted')

# Calculate indicators
ma20 = close.average(20)
ma60 = close.average(60)
rev_yoy = rev.pct_change(4)
pe = raw_close / eps.rolling(4).sum()

# Define conditions
c1 = (close > ma20) & (close > ma60)  # MA bullish
c2 = rev_yoy > 0.1  # Revenue growth
c3 = (pe > 0) & (pe < 20)  # Low P/E

# Exit signal
exit_signal = close < ma20

# -----------------------------------------------------------------------------
# Stage 3: Strategy Optimization
# -----------------------------------------------------------------------------

# Test condition combinations
conditions = {'c1': c1, 'c2': c2, 'c3': c3}
report_collection = sim_conditions(
    conditions=conditions,
    hold_until={'exit': exit_signal, 'stop_loss': 0.1},
    resample='M',
    position_limit=0.1,
    upload=False,
)

# Find the best combination
stats_df = report_collection.get_stats()
top1 = stats_df.T.sort_values('daily_sharpe', ascending=False).index[0]
print(f"Best combination: {top1}")

# Use the best combination
best_entry = c1 & c2 & c3

# Optimize stop loss/take profit
position_temp = best_entry.hold_until(exit=exit_signal, stop_loss=0.1)
report_temp = sim(position_temp, resample='M', position_limit=0.1, upload=False)
trades = report_temp.get_trades()

mae_q75 = abs(trades['mae'].quantile(0.75))
gmfe_q75 = trades['gmfe'].quantile(0.75)

# Final strategy
position_final = best_entry.hold_until(
    exit=exit_signal, stop_loss=mae_q75 * 1.2, take_profit=gmfe_q75 * 0.8
)

report_final = sim(
    position_final,
    resample='M',
    position_limit=0.1,
    upload=False,
    name="Revenue Growth Breakout v3.0",
)

# -----------------------------------------------------------------------------
# Stage 4: In-Depth Analysis
# -----------------------------------------------------------------------------

# Volatility analysis
report_final.display_mae_mfe_analysis()

# Period stability
report_final.run_analysis('PeriodStatsAnalysis')

# Alpha/Beta
report_final.run_analysis('AlphaBetaAnalysis')

# -----------------------------------------------------------------------------
# Stage 5: Out-of-Sample Testing
# -----------------------------------------------------------------------------

train_end = '2022-12-31'
test_start = '2023-01-01'

position_train = position_final[position_final.index <= train_end]
position_test = position_final[position_final.index >= test_start]

report_train = sim(position_train, resample='M', position_limit=0.1, upload=False)
report_test = sim(position_test, resample='M', position_limit=0.1, upload=False)

print("Training set Sharpe:", report_train.get_stats()['daily_sharpe'])
print("Test set Sharpe:    ", report_test.get_stats()['daily_sharpe'])

# -----------------------------------------------------------------------------
# Stage 6: Pre-Deployment Preparation
# -----------------------------------------------------------------------------

# Get position list
position_info = report_final.position_info()
metadata_keys = {
    'update_date',
    'next_trading_date',
    'trade_at',
    'freq',
    'market',
    'stop_loss',
    'take_profit',
}
stocks = {k: v for k, v in position_info.items() if k not in metadata_keys}
print("Next-period holdings:")
for symbol, info in stocks.items():
    print(f"  {symbol}: weight={info['weight']:.2%}")

To deploy, upload the final report with report_final.upload(name="Revenue Growth Breakout v3.0") (step 6), then connect a broker account and run steps 7 and 8 above.

Checklists

Development

  • Clear strategy hypothesis: Has a clear logical basis (technical/fundamental/institutional)
  • Data integrity check: Confirm data range is sufficient and missing value ratio is reasonable
  • Acceptable initial backtest performance: Annualized return > 10%, Sharpe ratio > 0.5
  • Condition combination optimization: Use sim_conditions() to find the best combination
  • Stop loss/take profit optimization: Use MAE/MFE analysis to set reasonable levels

Validation

  • Liquidity risk: Performance holds up after requiring enough dollar volume for your capital
  • Annual stability: No consecutive years of extremely poor performance
  • Alpha > 0: Strategy has excess returns
  • Out-of-sample test passed: Test set performance degradation < 30%, Sharpe ratio still > 1

Deployment

  • Strategy uploaded to cloud: Can review position list anytime
  • Broker account setup complete: API keys correct, permissions granted
  • Tested with simulated orders first: Confirmed order logic is correct
  • Small capital test: Tested with small amounts for 1-2 months
  • Regular review mechanism: Set up monthly live performance review

Common errors and how to check for them

When running the complete workflow, check for errors at each stage. These are the key checkpoints:

Steps 1-2: dataset not found, empty data, no trades

Common errors:

  • KeyError: 'price:adj_close' -- API token not set or incorrect data table name
  • Data is empty or has too many missing values
  • Backtest has no trade records (entry conditions too strict)
try:
    # 1. Check data loading
    data.set_market('us')
    close = data.get('price:adj_close')

    if close.empty:
        raise ValueError("❌ Data is empty; check your API token")

    missing_ratio = close.isna().sum().sum() / close.size
    if missing_ratio > 0.1:
        print(f"⚠️  Warning: missing value ratio {missing_ratio:.1%} > 10%")

    # 2. Check entry signals
    entry_count = entry_signal.sum(axis=1).mean()
    if entry_count < 1:
        print("⚠️  Warning: entry conditions too strict; average < 1 stock per day")
        print("Suggestion: loosen conditions or use .is_largest(N) for a fixed count")

    # 3. Check backtest results
    report = sim(position, resample='M', position_limit=0.1)
    trades = report.get_trades()

    if len(trades) == 0:
        raise ValueError("❌ Strategy produced no trades; check your entry conditions")

    print(f"✅ Backtest succeeded: {len(trades)} trades")

except KeyError as e:
    print(f"❌ Dataset not found: {e}")
    print("Visit https://studio.finlab.finance/en/database to confirm the dataset name")
    print("Or verify your API token is correctly set")

except ValueError as e:
    print(f"❌ Data validation failed: {e}")

For detailed error handling, see "Common errors and solutions" in Load data.

Step 3: too many combinations, inconsistent date ranges

Common errors:

  • sim_conditions() takes too long due to too many combinations
  • Condition definitions are incorrect (inconsistent date ranges)
# Limit the number of combinations (< 50)
conditions = {
    'c1': c1,
    'c2': c2,
    'c3': c3,
    # Maximum 5 conditions (2^5 = 32 combinations)
}

# Estimate execution time
num_combinations = 2 ** len(conditions) - 1
print(
    f"Will test {num_combinations} combinations, estimated time: ~{num_combinations * 30 / 60:.1f} minutes"
)

if num_combinations > 100:
    print("⚠️  Warning: too many combinations; test a subset of conditions first")

For detailed error handling, see Strategy optimization.

Step 5: overlapping training and test sets, large performance gap

Common errors:

  • Training and test set date ranges overlap
  • Test set performance differs too much from training set (overfitting)
# Check date ranges
train_end = '2022-12-31'
test_start = '2023-01-01'

position_train = position_final[position_final.index <= train_end]
position_test = position_final[position_final.index >= test_start]

# Confirm no overlap
if position_train.index[-1] >= position_test.index[0]:
    raise ValueError("❌ Training and test set dates overlap!")

# Check performance difference
sharpe_train = report_train.get_stats()['daily_sharpe']
sharpe_test = report_test.get_stats()['daily_sharpe']
degradation = (sharpe_train - sharpe_test) / sharpe_train

if degradation > 0.5:
    print(
        f"⚠️  Warning: Sharpe ratio degradation {degradation:.1%} > 50%; possible overfitting"
    )
    print("Suggestions:")
    print("1. Simplify strategy conditions")
    print("2. Extend the training data range")
    print("3. Use cross-validation")

For detailed error handling, see "Common errors and solutions" in Backtesting.

Step 7: connection failure, insufficient funds, rejected orders

Common errors:

  • Broker account connection failure (wrong API key, expired certificate)
  • Insufficient funds or too many positions
  • Orders rejected (insufficient buying power, trading halts; in Taiwan also disposition stocks and limit-up/down locks)

Pre-trading safety checks:

# Pre-trading safety checks
def pre_trading_checks(stocks, account, capital):
    """Safety checks before placing real orders"""

    print("=== Pre-trading safety checks ===\n")

    # 1. Number of holdings
    if len(stocks) > 30:
        raise ValueError(f"Too many holdings ({len(stocks)}); keep it under 30")

    # 2. Available cash
    available = account.get_cash()
    if capital > available * 0.9:
        raise ValueError(f"Not enough cash: need {capital:,.0f}, have {available:,.0f}")

    # 3. Broker connection
    try:
        account.get_position()
    except Exception as e:
        raise ConnectionError(f"Broker connection failed: {e}")

    print("All checks passed\n")

# Run the checks
try:
    pre_trading_checks(stocks, account, capital)

    # Preview first (places no orders)
    order_executor.create_orders(view_only=True)

    # Then place them (uncomment)
    # order_executor.create_orders()

except (ValueError, ConnectionError) as e:
    print(f"\n{e}")
    print("Fix the issues above before placing orders")

For detailed error handling, see "Common errors and solutions" in Live trading.

Next steps

Related pages: Backtesting, Risk management, FAQ, FinLab.

↑↓ Move Enter Open Esc Close