Skip to content

finlab.portfolio

Multi-strategy portfolio management module for combining multiple strategies into one portfolio, with cloud synchronization and automatic rebalancing support.

Use Cases

  • Risk diversification: Combine different strategy types (momentum + value + growth) to reduce the risk of a single strategy failing
  • Improved stability: Single strategies can be volatile; multi-strategy combinations effectively reduce maximum drawdown
  • Dynamic weight adjustment: Flexibly adjust strategy proportions based on market conditions or strategy performance
  • Live synchronization: Use PortfolioSyncManager to automatically update positions without manual management

Portfolio vs Single Strategy Backtest

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
Cloud sync Not supported Supported (PortfolioSyncManager)
Complexity Low Medium

Quick Examples

Basic Usage: Combine Three Strategies

from finlab import data
from finlab.backtest import sim
from finlab.portfolio import Portfolio

# Strategy 1: Momentum strategy (MA breakout)
close = data.get('price:收盤價')
ma20 = close.average(20)
position1 = close > ma20
report1 = sim(position1, resample='M', upload=False)

# Strategy 2: Value strategy (low P/B ratio)
pb = data.get('price_earning_ratio:股價淨值比')
position2 = pb.is_smallest(100)  # Select 100 smallest
report2 = sim(position2, resample='M', upload=False)

# Strategy 3: Revenue growth strategy
rev_yoy = data.get('monthly_revenue:去年同月增減(%)')
position3 = rev_yoy > 20
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%}")

Detailed Guide

See Multi-Strategy Portfolio Management Workflow for:

  • Strategy weight optimization methods (equal weight, risk parity, minimum volatility)
  • Dynamic weight adjustment strategies (based on recent performance)
  • Complete live synchronization workflow (PortfolioSyncManager)
  • Tips for avoiding high strategy correlation

API Reference

Portfolio

finlab.portfolio.Portfolio

Portfolio(reports)

Bases: Report

建構 Portfolio 物件。

PARAMETER DESCRIPTION
- reports

代表投資組合的字典,key 為資產名稱,value 是回測報告與部位。

TYPE: dict[str, tuple[Report, float]]

Example

組合多個策略

from finlab import sim
from finlab.portfolio import Portfolio

# 請參閱 sim 函數的文件以獲取更多信息
# https://doc.finlab.tw/getting-start/

report_strategy1 = sim(...)
report_strategy2 = sim(...)
report_strategy3 = sim(...)

portfolio = Portfolio({
    'strategy1': (report_strategy1, 0.3),
    'strategy2': (report_strategy2, 0.4),
    'strategy3': (report_strategy3, 0.3),
})

Weight Allocation Recommendations

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 volatility):

# First build equal-weight portfolio
portfolio = Portfolio({
    'Strategy A': (report1, 0.5),
    'Strategy B': (report2, 0.5)
})

# Compute portfolio volatility (Portfolio itself is a Report)
port_vol = portfolio.creturn.pct_change().std()

# Adjust weights to achieve target 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)
    })

Common Errors

1. Weights do not sum to 1

# Wrong: Total weight = 0.9
portfolio = Portfolio({
    'Strategy A': (report1, 0.4),
    'Strategy B': (report2, 0.5)  # Total 0.9
})

# Correct: Ensure weights sum to 1
portfolio = Portfolio({
    'Strategy A': (report1, 0.4),
    'Strategy B': (report2, 0.6)  # Total 1.0
})

2. Strategies not uploaded to cloud (required for PortfolioSyncManager)

# Wrong: Report not uploaded
report1 = sim(position1, resample='M', upload=False)

# Correct: Upload to cloud
report1 = sim(position1, resample='M', upload=True)

3. Inconsistent rebalancing frequency across strategies

# Wrong: Different rebalancing frequencies
report1 = sim(position1, resample='M')   # Monthly
report2 = sim(position2, resample='W')   # Weekly

# Correct: Unified rebalancing frequency
report1 = sim(position1, resample='M')   # Monthly
report2 = sim(position2, resample='M')   # Monthly

create_multi_asset_report()

finlab.portfolio.create_multi_asset_report

create_multi_asset_report(stock_list, **kwargs)

根據提供的股票清單創建多資產報告。 Create a multi-asset report based on the stock list provided.

PARAMETER DESCRIPTION
stock_list

一個以股票代號為 key,權重大小為 value。 A dictionary with stock id as key and weight as value.

TYPE: dict

RETURNS DESCRIPTION
Report

一個包含回測結果的報告對象。A report object with the backtest result.

TYPE: Report

Example:

>>> from finlab.portfolio import create_multi_asset_report
...
...
>>> report = create_multi_asset_report({'2330': 0.5, '1101': 0.5})

create_report_from_cloud()

finlab.portfolio.create_report_from_cloud

create_report_from_cloud(strategy_id, user_id=None, market=None)

根據提供的用戶ID和策略ID創建在線報告。 Create an online report based on the user id and strategy id provided.

PARAMETER DESCRIPTION
user_id

The user id.

TYPE: str DEFAULT: None

strategy_id

The

TYPE: str

PortfolioSyncManager

finlab.portfolio.PortfolioSyncManager

PortfolioSyncManager(data=None, start_with_empty=False)

投資組合同步管理器 -- 設定、儲存並同步投資組合部位。

See :meth:update for the main rebalancing entry point and :meth:sync / :meth:create_order_executor for order execution.

建構投資組合。

clear

clear()

清除持倉資訊。

RETURNS DESCRIPTION
None

None

create_order_executor

create_order_executor(account, at='close', consider_margin_as_asset=False, market_name=None, **kwargs)

建立 OrderExecutor(不自動下單)。

PARAMETER DESCRIPTION
account

交易帳戶。

TYPE: Account

consider_margin_as_asset

是否將融資融券視為資產。

TYPE: bool DEFAULT: False

market_name

指定市場名稱,None 代表所有市場。

TYPE: str | None DEFAULT: None

from_cloud classmethod

from_cloud(name='default')

從雲端檔案初始化投資組合。

PARAMETER DESCRIPTION
path

雲端檔案的路徑。預設為 'default'。

TYPE: str

RETURNS DESCRIPTION
PortfolioSyncManager

投資組合類別。

TYPE: PortfolioSyncManager

from_local classmethod

from_local(name='default')

從本地檔案初始化投資組合。

PARAMETER DESCRIPTION
path

本地檔案的路徑。

TYPE: str

RETURNS DESCRIPTION
PortfolioSyncManager

投資組合類別。

TYPE: PortfolioSyncManager

from_path classmethod

from_path(path)

從本地檔案初始化投資組合。

PARAMETER DESCRIPTION
path

本地檔案的路徑。

TYPE: str

RETURNS DESCRIPTION
PortfolioSyncManager

投資組合類別。

TYPE: PortfolioSyncManager

get_position

get_position(at='close', market_name=None)

獲取持倉資訊。

PARAMETER DESCRIPTION
market_name

指定市場名稱。預設為 None,也就是獲取所有市場。

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
Position

dict or Position: 若 combined 為 True,則返回合併後的持倉資訊(Position 物件); 若 combined 為 False,則返回原始持倉資訊(dict)。

get_strategy_position

get_strategy_position(strategy_name, at)

獲取策略的開倉部位。

PARAMETER DESCRIPTION
strategy_name

策略名稱。

TYPE: str

RETURNS DESCRIPTION
dict

開倉部位資訊。

TYPE: list[PositionEntryDict]

get_total_position

get_total_position()

回傳目前持倉的 DataFrame(與 repr 顯示的 df 相同)。

RETURNS DESCRIPTION
DataFrame

pd.DataFrame: 持倉資訊的 DataFrame。

sync

sync(account, at='close', consider_margin_as_asset=True, market_name=None, **kwargs)

同步持倉資訊並建立委託單。

PARAMETER DESCRIPTION
account

交易帳戶。

TYPE: Account

consider_margin_as_asset

是否將保證金交易視為資產。

TYPE: bool DEFAULT: True

market_name

指定市場名稱,None 代表所有市場。

TYPE: str | None DEFAULT: None

**kwargs

傳遞給 OrderExecutor.create_orders (market_order, best_price_limit, view_only, extra_bid_pct 等)。

TYPE: Unpack[CreateOrderKwargs] DEFAULT: {}

to_cloud

to_cloud(name='default')

將投資組合資訊存至雲端檔案。

RETURNS DESCRIPTION
Response

None

to_local

to_local(name='default')

將投資組合資訊存至本地檔案。

RETURNS DESCRIPTION
None

None

to_path

to_path(path)

將投資組合資訊存至本地檔案。

RETURNS DESCRIPTION
None

None

update

update(portfolio, total_balance=0, rebalance_safety_weight=0.2, smooth_transition=None, force_override_difference=False, custom_position=None, excluded_stock_ids=None, excluded_symbols=None, **kwargs)

設定投資組合的函數。

PARAMETER DESCRIPTION
portfolio

包含投資組合資訊的 Portfolio 或 Report 物件。

TYPE: Portfolio

total_balance

總資產。

TYPE: float DEFAULT: 0

rebalance_safety_weight

換股保留現金比例 (預設 0.2)。

TYPE: float DEFAULT: 0.2

smooth_transition

是否只在換股日才更新 (None 自動判斷)。

TYPE: bool | None DEFAULT: None

force_override_difference

是否強制覆蓋不同的部位。

TYPE: bool DEFAULT: False

custom_position

自定義部位 (不列入計算)。

TYPE: Position | dict[str, object] | None DEFAULT: None

excluded_stock_ids

排除的股票代碼列表。

TYPE: list[str] | None DEFAULT: None

excluded_symbols

排除的股票代碼列表 (同 excluded_stock_ids)。

TYPE: list[str] | None DEFAULT: None

Live Synchronization Best Practices

Basic setup:

from finlab.portfolio import PortfolioSyncManager

# Initialize (strategies must be uploaded to cloud first)
manager = PortfolioSyncManager(
    report_ids=['report_id_1', 'report_id_2'],  # Cloud strategy IDs
    weights=[0.5, 0.5],                         # Weights
    total_balance=1000000                        # Total capital 1M TWD
)

# Get current target positions and share counts
current_position = manager.get_position()
print(current_position)

Scheduled updates (recommended after market close daily):

import schedule
import time

def sync_portfolio():
    manager = PortfolioSyncManager(
        report_ids=['xxx', 'yyy'],
        weights=[0.6, 0.4],
        total_balance=1000000
    )
    position = manager.get_position()
    # Execute order logic (see finlab.online)
    print(f"Updated at: {time.strftime('%Y-%m-%d %H:%M:%S')}")
    print(position)

# Execute daily after market close (e.g., 15:00)
schedule.every().day.at("15:00").do(sync_portfolio)

while True:
    schedule.run_pending()
    time.sleep(60)

Cloud Synchronization Notes

  1. Ensure strategies are uploaded: Use report.upload() or sim(..., upload=True)
  2. Check position conflicts: Multiple strategies may simultaneously hold/short the same stock
    # Portfolio automatically handles conflicts:
    # - Strategy A: Holds 2330 (20% weight, strategy allocation 40%)
    # - Strategy B: Holds 2330 (15% weight, strategy allocation 60%)
    # -> Combined: 2330 total weight = 0.4 * 20% + 0.6 * 15% = 17%
    
  3. Regularly check sync status: Ensure cloud data matches local data

FAQ

Q: How are position conflicts between strategies handled?

Portfolio automatically handles position conflicts:

# Suppose two strategies both hold 2330
# Strategy A (weight 40%): 2330 at 20%
# Strategy B (weight 60%): 2330 at 15%

portfolio = Portfolio({
    'Strategy A': (report1, 0.4),
    'Strategy B': (report2, 0.6)
})

# Combined 2330 total weight = 0.4 * 0.20 + 0.6 * 0.15 = 0.17 (17%)

Q: 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 < 1.5).is_largest(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)
})

Q: How do I dynamically adjust strategy weights?

# Method 1: 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])
})

Q: Combined performance is worse than individual strategies?

Possible causes and how to investigate:

  1. 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)
    

  2. Poor weight allocation:

    # Check Sharpe ratio of each strategy
    print(f"Strategy 1 Sharpe: {report1.stats['daily_sharpe']:.2f}")
    print(f"Strategy 2 Sharpe: {report2.stats['daily_sharpe']:.2f}")
    
    # Give higher weight to strategies with higher Sharpe ratio
    

  3. Inconsistent backtest periods:

    # Ensure all strategies use the same backtest period
    import finlab
    finlab.truncate_start = '2018-01-01'
    
    report1 = sim(position1, resample='M')
    report2 = sim(position2, resample='M')
    # Ensure both reports have the same start date
    

Resources