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
Bases: Report
建構 Portfolio 物件。
| PARAMETER | DESCRIPTION |
|---|---|
- reports
|
代表投資組合的字典,key 為資產名稱,value 是回測報告與部位。
TYPE:
|
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
create_multi_asset_report()
finlab.portfolio.create_multi_asset_report
根據提供的股票清單創建多資產報告。 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:
|
| RETURNS | DESCRIPTION |
|---|---|
Report
|
一個包含回測結果的報告對象。A report object with the backtest result.
TYPE:
|
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
根據提供的用戶ID和策略ID創建在線報告。 Create an online report based on the user id and strategy id provided.
| PARAMETER | DESCRIPTION |
|---|---|
user_id
|
The user id.
TYPE:
|
strategy_id
|
The
TYPE:
|
PortfolioSyncManager
finlab.portfolio.PortfolioSyncManager
投資組合同步管理器 -- 設定、儲存並同步投資組合部位。
See :meth:update for the main rebalancing entry point and
:meth:sync / :meth:create_order_executor for order execution.
建構投資組合。
create_order_executor
create_order_executor(
account,
at='close',
consider_margin_as_asset=False,
market_name=None,
**kwargs,
)
建立 OrderExecutor(不自動下單)。
| PARAMETER | DESCRIPTION |
|---|---|
account
|
交易帳戶。
TYPE:
|
consider_margin_as_asset
|
是否將融資融券視為資產。
TYPE:
|
market_name
|
指定市場名稱,None 代表所有市場。
TYPE:
|
from_cloud
classmethod
從雲端檔案初始化投資組合。
| PARAMETER | DESCRIPTION |
|---|---|
path
|
雲端檔案的路徑。預設為 'default'。
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
PortfolioSyncManager
|
投資組合類別。
TYPE:
|
from_local
classmethod
從本地檔案初始化投資組合。
| PARAMETER | DESCRIPTION |
|---|---|
path
|
本地檔案的路徑。
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
PortfolioSyncManager
|
投資組合類別。
TYPE:
|
from_path
classmethod
從本地檔案初始化投資組合。
| PARAMETER | DESCRIPTION |
|---|---|
path
|
本地檔案的路徑。
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
PortfolioSyncManager
|
投資組合類別。
TYPE:
|
get_position
獲取持倉資訊。
| PARAMETER | DESCRIPTION |
|---|---|
market_name
|
指定市場名稱。預設為 None,也就是獲取所有市場。
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Position
|
dict or Position: 若 combined 為 True,則返回合併後的持倉資訊(Position 物件); 若 combined 為 False,則返回原始持倉資訊(dict)。 |
get_strategy_position
獲取策略的開倉部位。
| PARAMETER | DESCRIPTION |
|---|---|
strategy_name
|
策略名稱。
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict
|
開倉部位資訊。
TYPE:
|
get_total_position
回傳目前持倉的 DataFrame(與 repr 顯示的 df 相同)。
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
pd.DataFrame: 持倉資訊的 DataFrame。 |
sync
同步持倉資訊並建立委託單。
| PARAMETER | DESCRIPTION |
|---|---|
account
|
交易帳戶。
TYPE:
|
consider_margin_as_asset
|
是否將保證金交易視為資產。
TYPE:
|
market_name
|
指定市場名稱,None 代表所有市場。
TYPE:
|
**kwargs
|
傳遞給
TYPE:
|
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:
|
total_balance
|
總資產。
TYPE:
|
rebalance_safety_weight
|
換股保留現金比例 (預設 0.2)。
TYPE:
|
smooth_transition
|
是否只在換股日才更新 (None 自動判斷)。
TYPE:
|
force_override_difference
|
是否強制覆蓋不同的部位。
TYPE:
|
custom_position
|
自定義部位 (不列入計算)。
TYPE:
|
excluded_stock_ids
|
排除的股票代碼列表。
TYPE:
|
excluded_symbols
|
排除的股票代碼列表 (同 excluded_stock_ids)。
TYPE:
|
| 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
- Ensure strategies are uploaded: Use
report.upload()orsim(..., upload=True) - Check position conflicts: Multiple strategies may simultaneously hold/short the same stock
- 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:
-
High strategy correlation:
-
Poor weight allocation:
-
Inconsistent backtest periods:
Resources
- Multi-Strategy Portfolio Management Workflow - In-depth guide
- Complete Strategy Development Workflow - From research to live trading
- Risk Management Guide - Portfolio-level risk control
- Live Trading Tutorial (Multi-Strategy) - Portfolio live execution
- GitHub Source Code