API reference
finlab.portfolio
On this page8
finlab.portfolio combines several backtest reports into one portfolio, using the weights you give. The portfolio is itself a report, so display() and get_stats() work on it directly. For live trading, PortfolioSyncManager turns the portfolio into share quantities and syncs a broker account to them.
Which function to use
| You want to | Use |
|---|---|
| Combine strategies with fixed weights | Portfolio() |
| Let strategy weights change over time | Portfolio.from_weight_function() |
| Backtest a fixed-weight basket of stocks or ETFs | create_multi_asset_report() |
| Load a strategy report saved in FinLab's cloud | create_report_from_cloud() |
| Turn the portfolio into quantities and sync a broker account | PortfolioSyncManager |
| Check whether any strategy has a pending stop exit | portfolio.is_stop_triggered() |
When to use a portfolio
Combining different strategy types (momentum + value + growth) reduces the risk of a single strategy failing, and usually lowers the maximum drawdown. You can adjust the weights to market conditions or strategy performance, and for live trading PortfolioSyncManager turns the combined targets into share quantities and syncs a broker account.
| Feature | Single strategy (sim) | Portfolio |
|---|---|---|
| Use case | Individual strategy research | Multi-strategy combination management |
| Input | position DataFrame | Multiple Report objects |
| Weight allocation | Automatic equal weight | Customizable per-strategy weights |
| Risk control | Single strategy level | Portfolio-level rebalancing |
| Live sync | Position.from_report() + OrderExecutor | PortfolioSyncManager |
| Complexity | Low | Medium |
Combine strategies
Portfolio()
Combines several backtest reports into one portfolio. Portfolio is itself a Report, so display(), get_stats(), creturn and the other report tools work on the combined result.
from finlab import data
from finlab.backtest import sim
from finlab.portfolio import Portfolio
data.set_market('us')
with data.us_universe(index='SP500'):
close = data.get('price:adj_close')
market_value = data.get('etl:us_market_value')
equity = data.get('us_balance_sheet:total_stockholders_equity')
revenue = data.get('us_income_statement:revenue')
# Strategy 1: Momentum strategy (MA breakout)
ma20 = close.average(20)
strength = close / ma20
position1 = strength[strength > 1].is_largest(
30
) # 30 stocks furthest above their 20-day average
report1 = sim(position1, resample='M', upload=False)
# Strategy 2: Value strategy (low P/B ratio)
pb = market_value / equity
position2 = pb[pb > 0].is_smallest(30) # 30 lowest positive P/B
report2 = sim(position2, resample='M', upload=False)
# Strategy 3: Revenue growth strategy (quarterly revenue, year over year)
rev_yoy = revenue.pct_change(4)
position3 = (rev_yoy > 0.2) & (
close > close.average(60)
) # aligned to daily filing dates
report3 = sim(position3, resample='M', upload=False)
# Build portfolio (weights: 40%, 30%, 30%)
portfolio = Portfolio(
{
'Momentum': (report1, 0.4),
'Value': (report2, 0.3),
'Revenue Growth': (report3, 0.3),
}
)
# Portfolio itself is a Report, use it directly
portfolio.display()
# View portfolio statistics
stats = portfolio.get_stats()
print(f"Portfolio annual return: {stats['cagr']:.2%}")
print(f"Portfolio Sharpe ratio: {stats['daily_sharpe']:.2f}")
print(f"Portfolio max drawdown: {stats['max_drawdown']:.2%}")| Parameter | Type | Default | Description |
|---|---|---|---|
reports | dict[str, tuple[Report, float]] or Report | required | Strategy name to (report, weight). A single Report is treated as one strategy with weight 1.0. Strategies with weight 0 are ignored; at least one weight must be nonzero. |
Weights need not sum to 1. Daily returns are the weighted sum of the strategies' returns; weights are not rescaled, so a total below 1 leaves the rest in cash:
# Total weight 0.9: the remaining 10% stays in cash and earns nothing
portfolio = Portfolio({'Strategy A': (report1, 0.4), 'Strategy B': (report2, 0.5)})
# Fully invested: weights sum to 1
portfolio = Portfolio({'Strategy A': (report1, 0.4), 'Strategy B': (report2, 0.6)})Use one market per report. Each report should come from one market (data.set_market('us') for stocks, 'us_fund' for ETFs). Combine a stock report and an ETF report in a Portfolio instead of mixing their tickers in one sim() call.
How the combined properties are computed:
Portfolio.positionuses the union of the strategies' rebalance dates, carrying each strategy's latest target position forward and keeping its allocation in cash before its first target.Portfolio.creturnstarts at 1 on the first date every nonzero-weight strategy covers and ends on the earliest last date, so its period may be shorter than the position history. A strategy with no row on a date in between (its market was closed) earns 0 that day while the others still count.Portfolio.tradespositions are shares of the whole portfolio (the strategy's position times its weight), and capacity is computed from them.
Choose the weights
Equal weight (suitable when strategies have similar characteristics):
# 3 strategies, 1/3 each
portfolio = Portfolio(
{
'Strategy A': (report1, 1 / 3),
'Strategy B': (report2, 1 / 3),
'Strategy C': (report3, 1 / 3),
}
)Risk parity (inversely weighted by strategy volatility):
# Compute volatility for each strategy
vol1 = report1.creturn.pct_change().std()
vol2 = report2.creturn.pct_change().std()
vol3 = report3.creturn.pct_change().std()
# Inverse weights (lower volatility gets higher weight)
total_inv_vol = 1 / vol1 + 1 / vol2 + 1 / vol3
w1, w2, w3 = (
(1 / vol1) / total_inv_vol,
(1 / vol2) / total_inv_vol,
(1 / vol3) / total_inv_vol,
)
portfolio = Portfolio(
{
'Strategy A': (report1, w1),
'Strategy B': (report2, w2),
'Strategy C': (report3, w3),
}
)Target volatility (set portfolio target annualized volatility):
# First build equal-weight portfolio
portfolio = Portfolio({'Strategy A': (report1, 0.5), 'Strategy B': (report2, 0.5)})
# Compute annualized portfolio volatility (assuming 252 trading days per year)
port_vol = portfolio.creturn.pct_change().std() * 252**0.5
# Adjust weights to achieve target annualized volatility (e.g., 15%)
target_vol = 0.15
leverage = target_vol / port_vol # Leverage ratio
# Reconfigure (ensure total weight <= 1)
if leverage <= 1:
portfolio = Portfolio(
{
'Strategy A': (report1, 0.5 * leverage),
'Strategy B': (report2, 0.5 * leverage),
}
)Portfolio.from_weight_function()
Builds a portfolio from a weight history, so each strategy's weight can change over time.
import pandas as pd
from finlab.portfolio import Portfolio
def weights():
# One column per strategy name; each row takes effect from its date
return pd.DataFrame(
{'Momentum': [0.5, 0.7], 'Value': [0.5, 0.3]},
index=pd.to_datetime(['2020-01-02', '2023-01-03']),
)
portfolio = Portfolio.from_weight_function(
{'Momentum': report1, 'Value': report2}, weights
)| Parameter | Type | Default | Description |
|---|---|---|---|
reports | dict[str, Report] | required | Strategy name to backtest report. Every name must be a column of the weight table. |
weight_function | function with no arguments | required | Returns a DataFrame of weights over time, one column per report name. The latest row must not contain NaN. |
Returns are computed with the weight history: each row is rebalanced into at the close of its date (or of the next trading day if its date is not one), earns from the following trading day, and holds until the next row. The latest row sets the current weights.
portfolio.is_stop_triggered()
Returns True if any strategy has a pending stop-loss or take-profit exit.
if portfolio.is_stop_triggered():
print('A strategy has a pending stop exit')Backtest a fixed-weight basket
create_multi_asset_report()
Backtests a fixed-weight basket and returns its Report, for example to hold a set of stocks or ETFs and rebalance them periodically.
from finlab import data
from finlab.portfolio import create_multi_asset_report
with data.market('us_fund'): # ETFs trade in the U.S. fund market
report = create_multi_asset_report({'SPY': 0.6, 'QQQ': 0.4}, resample='M')| Parameter | Type | Default | Description |
|---|---|---|---|
stock_list | dict[str, float] | required | Symbol to weight, for example {'AAPL': 0.5, 'MSFT': 0.5}. |
**kwargs | Passed to backtest.sim(). resample defaults to 'Q' (quarterly rebalancing); upload is always False. |
Load a strategy from the cloud
create_report_from_cloud()
Loads a strategy report saved in FinLab's cloud (for example a strategy listed under Strategies in FinLab Studio) and returns it as a CloudReport, which can be used like any Report, including in a Portfolio.
from finlab.portfolio import Portfolio, create_report_from_cloud
report_a = create_report_from_cloud('Strategy A')
report_b = create_report_from_cloud('Strategy B')
portfolio = Portfolio({'Strategy A': (report_a, 0.5), 'Strategy B': (report_b, 0.5)})| Parameter | Type | Default | Description |
|---|---|---|---|
strategy_id | str | required | Name of the cloud strategy. |
user_id | str | None | Owner of the strategy; defaults to the logged-in user. |
market | Market | None | Market of the strategy; detected from the saved report when omitted. |
Sync live holdings
PortfolioSyncManager()
Keeps the live target holdings of a Portfolio: on each strategy's rebalance date it turns the strategy's weights into share quantities within the capital you provide, applies stop-loss and take-profit exits between rebalances, saves its state locally or in FinLab's cloud, and syncs a broker account to the result. Usually created empty or loaded with from_local() / from_cloud().
Run it once a day, after the close and before the next open:
from finlab.online.schwab_account import SchwabAccount
from finlab.portfolio import Portfolio, PortfolioSyncManager
account = SchwabAccount()
portfolio = Portfolio({'Momentum': (report1, 0.5), 'Value': (report2, 0.5)})
pm = PortfolioSyncManager.from_local()
pm.update(portfolio, total_balance=account.get_total_balance())
pm.to_local()
print(pm.get_position()) # target share quantities
pm.sync(account, view_only=True) # preview; drop view_only to place ordersThe first run starts from PortfolioSyncManager() and pm.to_local() instead of from_local(). The constructor's full signature is PortfolioSyncManager(data=None, start_with_empty=False).
Save and load the state
Load or save the manager's state in FinLab's local workspace directory, in FinLab's cloud (requires login), or at a file path.
pm = PortfolioSyncManager.from_cloud('default')
pm.to_cloud('default')| Method | Default | Description |
|---|---|---|
PortfolioSyncManager.from_local(name) / to_local(name) | name='default' | FinLab's local workspace directory. |
PortfolioSyncManager.from_cloud(name) / to_cloud(name) | name='default' | FinLab's cloud (requires login). |
PortfolioSyncManager.from_path(path) / to_path(path) | A file path you choose. |
pm.update()
Recalculates the target holdings. Run it every day; see the parameter guide in the multi-strategy order tutorial.
pm.update(portfolio, total_balance=account.get_total_balance(), excluded_symbols=['TSLA'])| Parameter | Type | Default | Description |
|---|---|---|---|
portfolio | Portfolio or Report | required | Strategies to deploy. |
total_balance | float | 0 | Total capital managed by this manager, supplied by you (it is never read from the broker); pass account.get_total_balance() to let it follow the account. |
rebalance_safety_weight | float | 0.2 | Share of the freed capital kept in cash on a rebalance. |
smooth_transition | bool | None | Apply added, removed or reweighted strategies only on each strategy's next rebalance date; None decides automatically. |
force_override_difference | bool | False | Overwrite holdings that differ from the strategy report even on a non-rebalance day. |
custom_position | Position or dict | None | Extra holdings to keep, excluded from the calculation. |
excluded_stock_ids / excluded_symbols | list[str] | None | Symbols never to hold. |
update() raises finlab.exceptions.PortfolioError without changing anything when it has to rebuild a strategy but total_balance leaves no capital after the holdings of the strategies it is not updating; raise total_balance and run it again.
pm.get_position()
Returns the combined target Position of all strategies, or of one market. at defaults to 'close'.
pm.get_position()| Parameter | Type | Default | Description |
|---|---|---|---|
market_name | str | None | Only this market; None returns all markets. |
pm.get_strategy_position()
Returns the target holdings of one strategy.
pm.get_strategy_position('Momentum', at='close')| Parameter | Type | Default | Description |
|---|---|---|---|
strategy_name | str | required | Name of the strategy in the portfolio. |
at | str | required | As in get_position(), but with no default here. |
pm.get_total_position()
Taiwan market: returns a holdings table with prices, weights and the strategies holding each stock.
pm.get_total_position()pm.sync()
Syncs the broker account to the target and returns the OrderExecutor.
pm.sync(account, view_only=True) # preview
pm.sync(account, extra_bid_pct=0.01) # place orders| Parameter | Type | Default | Description |
|---|---|---|---|
account | Account | required | A logged-in broker account. |
at | str | 'close' | As in get_position(). |
consider_margin_as_asset | bool | True | Count margin positions as assets. |
market_name | str | None | Only this market; None syncs all markets. |
**kwargs | Passed to OrderExecutor.create_orders() (view_only, market_order, extra_bid_pct, ...). |
pm.create_order_executor()
Builds the OrderExecutor without placing orders, so you can inspect or place them yourself.
executor = pm.create_order_executor(account)
executor.create_orders(view_only=True)| Parameter | Type | Default | Description |
|---|---|---|---|
account | Account | required | A logged-in broker account. |
at | str | 'close' | As in get_position(). |
consider_margin_as_asset | bool | False | Count margin positions as assets. |
market_name | str | None | Only this market; None covers all markets. |
pm.clear()
Removes all saved holdings.
pm.clear()Common questions
What happens when two strategies hold the same stock?
Portfolio combines them automatically:
# Suppose two strategies both hold AAPL
# Strategy A (weight 40%): AAPL at 20%
# Strategy B (weight 60%): AAPL at 15%
portfolio = Portfolio({'Strategy A': (report1, 0.4), 'Strategy B': (report2, 0.6)})
# Combined AAPL total weight = 0.4 * 0.20 + 0.6 * 0.15 = 0.17 (17%)How do I set a maximum number of holdings?
Use is_largest() in each strategy's backtest to limit holdings:
# Strategy 1: Max 30 stocks
position1 = (close > ma20).is_largest(30)
report1 = sim(position1, resample='M')
# Strategy 2: Max 20 stocks
position2 = pb[pb > 0].is_smallest(20)
report2 = sim(position2, resample='M')
# Combined: approximately 50 stocks max (may overlap)
portfolio = Portfolio({'Strategy 1': (report1, 0.5), 'Strategy 2': (report2, 0.5)})How do I weight strategies by recent performance?
# Adjust based on recent performance
def dynamic_weights(reports, window=60):
"""Adjust weights based on last 60 days of performance"""
recent_returns = []
for report in reports:
ret = report.creturn.pct_change().tail(window).mean() # Recent average return
recent_returns.append(max(ret, 0)) # Set negative returns to 0
# Normalize
total = sum(recent_returns)
if total == 0:
return [1 / len(reports)] * len(reports) # Equal weight
return [r / total for r in recent_returns]
# Use dynamic weights
weights = dynamic_weights([report1, report2, report3], window=90)
portfolio = Portfolio(
{
'Strategy 1': (report1, weights[0]),
'Strategy 2': (report2, weights[1]),
'Strategy 3': (report3, weights[2]),
}
)For weights that change during the backtest itself, use Portfolio.from_weight_function() above.
Why is the combined result worse than the individual strategies?
Possible causes and how to investigate:
High strategy correlation:
# Check strategy correlation returns1 = report1.creturn.pct_change() returns2 = report2.creturn.pct_change() correlation = returns1.corr(returns2) print(f"Correlation: {correlation:.2f}") # > 0.8 indicates high correlation # Solution: Choose strategies with low correlation (< 0.5)Poor weight allocation:
# Check Sharpe ratio of each strategy print(f"Strategy 1 Sharpe: {report1.get_stats()['daily_sharpe']:.2f}") print(f"Strategy 2 Sharpe: {report2.get_stats()['daily_sharpe']:.2f}") # Give higher weight to strategies with higher Sharpe ratioInconsistent backtest periods:
# Ensure all strategies use the same backtest period from finlab import data data.truncate_start = '2018-01-01' report1 = sim(position1, resample='M') report2 = sim(position2, resample='M') # Ensure both reports have the same start date
Next steps
- Multi-strategy portfolios Weight methods (equal weight, risk parity, minimum volatility), weighting by recent performance, the PortfolioSyncManager live workflow, and avoiding correlated strategies.
- Live trading, multiple strategies The full live workflow and the
update()parameter guide. - Risk management Portfolio-level risk control.
- Complete strategy workflow From research to live trading.
Source code is on GitHub.
Was this page helpful?