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

Portfolio.position uses 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.creturn covers only dates with returns from every nonzero-weight strategy, so its period may be shorter than the position history.

finlab.portfolio.Portfolio

Portfolio(reports)

Bases: Report

建構 Portfolio 物件。

PARAMETER DESCRIPTION
- reports

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

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

Notes

部位採各子報告換股日期的聯集,延用最近一次目標;尚未開始的策略權重留作現金。 組合報酬只計算所有非零權重策略都有報酬的共同期間,可能比部位期間短。 Positions use the union of rebalance dates, carrying each strategy's latest target forward with cash before its first target. Cumulative returns cover only dates with returns from every nonzero-weight strategy, so their period may be shorter than the position history.

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

RAISES DESCRIPTION
PortfolioError

本次確實重建正權重 sleeve,且 total_balance 減去 本次不更新 sleeve 的既有持股市值 <= 1e-6(絕對貨幣容差, 遠小於 0.01,將等號與浮點微差一致視為無資金)。沒有重建、 只移除或只停損停利時不受此檢查限制;總額與保留市值皆為 0 的明確零預算沿用原行為。請依例外中的下限調高 total_balance, 並預留目標股票與安全現金所需資金,再重跑 update()。拋錯時 整次更新中止:不寫 config/history、不移除或重建部位,也不 處理任何 sleeve(包括其他 sleeve)的停損停利。

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