Skip to content

Multi-Strategy Portfolio Management Complete Workflow

This document introduces how to build, manage, and deploy multi-strategy investment portfolios, covering the complete workflow from strategy pool creation to live execution.

Why Multi-Strategy Portfolios?

Risks of a single strategy:

  • Market regime changes: A strategy may perform well in certain market conditions but fail when conditions change
  • Drawdown risk: A single strategy's maximum drawdown can be very large
  • Unstable returns: A strategy may underperform for extended periods

Advantages of multi-strategy portfolios:

  • Risk diversification: Different strategies complement each other across different market environments
  • Reduced drawdowns: Portfolio maximum drawdown is typically smaller than any single strategy
  • Stable returns: Smooths out individual strategy volatility

Workflow Overview

graph TB
    A[建立策略池] --> B{有多個策略?}
    B -->|是| C[使用 Portfolio 組合]
    B -->|否| A
    C --> D[回測組合績效]
    D --> E{績效滿意?}
    E -->|否| F[調整權重或策略]
    F --> C
    E -->|是| G[使用 PortfolioSyncManager]
    G --> H[計算持股張數]
    H --> I[實盤執行]
    I --> J[定期調整]
    J --> K{需要優化?}
    K -->|是| F
    K -->|否| I

Stage 1: Building the Strategy Pool

1.1 Develop Multiple Strategies of Different Types

A recommended portfolio includes strategies of different styles:

from finlab import data
from finlab.backtest import sim

# Strategy 1: Technical momentum strategy (short-term)
close = data.get('price:收盤價')
volume = data.get('price:成交股數')

entry1 = (close == close.rolling(20).max()) & (volume > volume.average(20) * 1.5)
exit1 = close < close.average(10)

position1 = entry1.hold_until(exit=exit1, stop_loss=0.08)
report1 = sim(position1, resample='W', name="動能策略", upload=False)

# Strategy 2: Fundamental value strategy (medium-term)
pb = data.get('price_earning_ratio:股價淨值比')
roe = data.get('fundamental_features:股東權益報酬率')

entry2 = (pb < pb.quantile(0.3, axis=1)) & (roe > 0.1)
exit2 = pb > pb.quantile(0.7, axis=1)

position2 = entry2.hold_until(exit=exit2)
report2 = sim(position2, resample='M', name="價值策略", upload=False)

# Strategy 3: Revenue growth strategy (medium to long-term)
rev = data.get('monthly_revenue:當月營收')
rev_ma3 = rev.average(3)
rev_ma12 = rev.average(12)

entry3 = (rev_ma3 / rev_ma12 > 1.15) & (close > close.average(60))
exit3 = rev_ma3 / rev_ma12 < 1.0

position3 = entry3.hold_until(exit=exit3, stop_loss=0.1)
report3 = sim(position3, resample='M', name="營收成長策略", upload=False)

print("策略池建立完成!")
print(f"策略 1 年化報酬: {report1.get_stats()['daily_mean']*100:.2f}%")
print(f"策略 2 年化報酬: {report2.get_stats()['daily_mean']*100:.2f}%")
print(f"策略 3 年化報酬: {report3.get_stats()['daily_mean']*100:.2f}%")

1.2 Load Previously Developed Strategies from Cloud

from finlab.portfolio import create_report_from_cloud

# Load previously uploaded strategies from the FinLab platform
report1 = create_report_from_cloud('我的動能策略')
report2 = create_report_from_cloud('我的價值策略')
report3 = create_report_from_cloud('我的營收成長策略')

print("從雲端載入 3 個策略完成!")

Stage 2: Combining Strategies with the Portfolio Module

2.1 Basic Portfolio: Equal Weight

from finlab.portfolio import Portfolio

# Create equal-weight portfolio (1/3 each)
port_equal = Portfolio({
    '動能策略': (report1, 1/3),
    '價值策略': (report2, 1/3),
    '營收成長策略': (report3, 1/3)
})

# Display portfolio performance
port_equal.display()

2.2 Advanced Portfolio: Sharpe Ratio-Weighted

# Get Sharpe ratio for each strategy
sharpe1 = report1.get_stats()['daily_sharpe']
sharpe2 = report2.get_stats()['daily_sharpe']
sharpe3 = report3.get_stats()['daily_sharpe']

# Calculate weights (higher Sharpe = higher weight)
total_sharpe = sharpe1 + sharpe2 + sharpe3
w1 = sharpe1 / total_sharpe
w2 = sharpe2 / total_sharpe
w3 = sharpe3 / total_sharpe

print(f"動能策略權重: {w1*100:.1f}%")
print(f"價值策略權重: {w2*100:.1f}%")
print(f"營收成長策略權重: {w3*100:.1f}%")

# Create Sharpe-weighted portfolio
port_sharpe = Portfolio({
    '動能策略': (report1, w1),
    '價值策略': (report2, w2),
    '營收成長策略': (report3, w3)
})

port_sharpe.display()

2.3 Portfolio Performance Evaluation

# Get portfolio performance
stats_equal = port_equal.report.get_stats()
stats_sharpe = port_sharpe.report.get_stats()

# Compare with individual strategies
import pandas as pd

comparison = pd.DataFrame({
    '動能策略': [
        report1.get_stats()['daily_mean'],
        report1.get_stats()['daily_sharpe'],
        report1.get_stats()['max_drawdown']
    ],
    '價值策略': [
        report2.get_stats()['daily_mean'],
        report2.get_stats()['daily_sharpe'],
        report2.get_stats()['max_drawdown']
    ],
    '營收成長策略': [
        report3.get_stats()['daily_mean'],
        report3.get_stats()['daily_sharpe'],
        report3.get_stats()['max_drawdown']
    ],
    '等權組合': [
        stats_equal['daily_mean'],
        stats_equal['daily_sharpe'],
        stats_equal['max_drawdown']
    ],
    '夏普加權組合': [
        stats_sharpe['daily_mean'],
        stats_sharpe['daily_sharpe'],
        stats_sharpe['max_drawdown']
    ]
}, index=['年化報酬率', '夏普率', '最大回撤'])

print(comparison)

# Example output:
#            動能策略   價值策略  營收成長策略  等權組合  夏普加權組合
# 年化報酬率   0.185    0.142      0.168    0.165      0.172
# 夏普率       1.23     0.98       1.15     1.35       1.42
# 最大回撤    -0.312   -0.285     -0.298   -0.245     -0.238

Portfolio Advantages

Notice the portfolio Sharpe ratios (1.35, 1.42) are higher than all individual strategies, and max drawdowns (-0.245, -0.238) are also smaller, confirming the diversification effect!


Stage 3: Backtesting the Multi-Strategy Portfolio

3.1 In-Depth Portfolio Analysis

# Get the portfolio backtest report
combined_report = port_sharpe.report

# Liquidity analysis
combined_report.run_analysis('LiquidityAnalysis', required_volume=100000)

# MAE/MFE analysis
combined_report.display_mae_mfe_analysis()

# Period stability
combined_report.run_analysis('PeriodStatsAnalysis')

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

3.2 Check Portfolio Holding Diversification

# Get portfolio positions
position_combined = combined_report.position

# Calculate average number of holdings
avg_holdings = (position_combined > 0).sum(axis=1).mean()
print(f"平均持股數: {avg_holdings:.1f}")

# Calculate maximum single holding weight
max_weight = position_combined.max(axis=1).mean()
print(f"平均最大單一持股權重: {max_weight*100:.1f}%")

# Check overlap among three strategies
pos1 = report1.position.iloc[-1]
pos2 = report2.position.iloc[-1]
pos3 = report3.position.iloc[-1]

overlap_12 = len(set(pos1[pos1>0].index) & set(pos2[pos2>0].index))
overlap_13 = len(set(pos1[pos1>0].index) & set(pos3[pos3>0].index))
overlap_23 = len(set(pos2[pos2>0].index) & set(pos3[pos3>0].index))

print(f"動能 & 價值 重疊股數: {overlap_12}")
print(f"動能 & 營收成長 重疊股數: {overlap_13}")
print(f"價值 & 營收成長 重疊股數: {overlap_23}")

Stage 4: Dynamic Weight Adjustment

4.1 Rolling Window Weight Recalculation

def optimize_weights(reports, lookback_days=252):
    """
    Dynamically adjust weights based on past year performance

    Args:
        reports: list of Report objects
        lookback_days: lookback period (default 252 trading days ~ 1 year)

    Returns:
        dict: optimal weights
    """
    import numpy as np
    from scipy.optimize import minimize

    # Get daily returns for the past year
    returns = []
    for report in reports:
        daily_return = report.daily_creturn.pct_change().iloc[-lookback_days:]
        returns.append(daily_return)

    returns_df = pd.concat(returns, axis=1).dropna()

    # Calculate covariance matrix
    cov_matrix = returns_df.cov()

    # Minimize variance (risk parity)
    def portfolio_variance(weights):
        return np.dot(weights, np.dot(cov_matrix, weights))

    # Constraints: weights sum to 1, each weight >= 0
    constraints = {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
    bounds = tuple((0, 1) for _ in range(len(reports)))

    # Initial weights
    init_weights = np.array([1/len(reports)] * len(reports))

    # Optimize
    result = minimize(
        portfolio_variance,
        init_weights,
        method='SLSQP',
        bounds=bounds,
        constraints=constraints
    )

    return result.x

# Calculate optimal weights
optimal_weights = optimize_weights([report1, report2, report3])

print("最優權重:")
for i, w in enumerate(optimal_weights, 1):
    print(f"  策略 {i}: {w*100:.1f}%")

# Build optimally weighted portfolio
port_optimal = Portfolio({
    '動能策略': (report1, optimal_weights[0]),
    '價值策略': (report2, optimal_weights[1]),
    '營收成長策略': (report3, optimal_weights[2])
})

port_optimal.display()

4.2 Dynamic Switching Based on Market Conditions

def adaptive_weights(reports, market_condition='bull'):
    """
    Adjust weights based on market conditions

    Args:
        reports: list of (name, Report) tuples
        market_condition: 'bull', 'bear', or 'sideways'

    Returns:
        dict: adjusted weights
    """
    if market_condition == 'bull':
        # Bull market: increase momentum strategy weight
        return [0.5, 0.25, 0.25]
    elif market_condition == 'bear':
        # Bear market: increase value strategy weight
        return [0.2, 0.5, 0.3]
    else:  # sideways
        # Sideways: balanced allocation
        return [0.33, 0.33, 0.34]

# Determine market condition (simplified example)
from finlab import data

taiex = data.get('benchmark_return:發行量加權股價報酬指數').squeeze()
taiex_ma50 = taiex.rolling(50).mean()
taiex_ma200 = taiex.rolling(200).mean()

if taiex.iloc[-1] > taiex_ma50.iloc[-1] > taiex_ma200.iloc[-1]:
    market = 'bull'
    print("市場環境:多頭")
elif taiex.iloc[-1] < taiex_ma50.iloc[-1] < taiex_ma200.iloc[-1]:
    market = 'bear'
    print("市場環境:空頭")
else:
    market = 'sideways'
    print("市場環境:盤整")

# Adjust weights based on market conditions
adaptive_w = adaptive_weights([report1, report2, report3], market)

port_adaptive = Portfolio({
    '動能策略': (report1, adaptive_w[0]),
    '價值策略': (report2, adaptive_w[1]),
    '營收成長策略': (report3, adaptive_w[2])
})

Stage 5: Live Execution

5.1 Using PortfolioSyncManager

from finlab.portfolio import PortfolioSyncManager

# Create for the first time
pm = PortfolioSyncManager()

# Or load existing positions from cloud/local
# pm = PortfolioSyncManager.from_cloud()
# pm = PortfolioSyncManager.from_local()

5.2 Update Holdings

# Set total capital
total_balance = 5000000  # 5 million TWD

# Update holdings (rebalancing only happens on rebalance dates)
pm.update(port_sharpe, total_balance=total_balance)

# Display current holdings
print(pm)

# Example output:
# Estimate value: 5,123,450
#
#          quantity  price   weight  close_price  volume  strategy         type
# stock_id
# 2330         50.0  520.0   0.052       520.0     1250   策略1,策略3    STOCK
# 2317        100.0   85.0   0.017        85.0      850   策略2          STOCK
# 2454         80.0  120.0   0.019       120.0      960   策略1          STOCK
# ...

5.3 Enable Odd Lot or Margin Trading

# Odd lot trading (can buy/sell fractional shares)
pm.update(port_sharpe, total_balance=total_balance, odd_lot=True)

# Margin trading (can use margin and short selling)
pm.update(port_sharpe, total_balance=total_balance, margin_trading=True)

# Combined
pm.update(
    port_sharpe,
    total_balance=total_balance,
    odd_lot=True,
    margin_trading=True
)

5.4 Live Order Execution

from finlab.online.sinopac_account import SinopacAccount
from finlab.online.order_executor import OrderExecutor

# Set up broker account (environment variables must be configured first; see live trading tutorial)
account = SinopacAccount()

# Use pm.sync to directly sync positions and place orders
pm.sync(account)

# Output:
# 2024-12-20 09:05:23 [INFO] 開始執行換股
# 2024-12-20 09:05:25 [INFO] 賣出 2303 100 股 @ 10.5
# 2024-12-20 09:05:27 [INFO] 買入 2330 50 股 @ 519.5
# ...

5.5 Scheduled Execution Script

# Create a daily execution script
def daily_portfolio_sync():
    """
    Daily execution:
    1. Check if today is a rebalance date
    2. Check stop loss/take profit
    3. Update holdings
    """
    from finlab.portfolio import PortfolioSyncManager, create_report_from_cloud

    # Load strategies
    report1 = create_report_from_cloud('動能策略')
    report2 = create_report_from_cloud('價值策略')
    report3 = create_report_from_cloud('營收成長策略')

    # Build portfolio
    port = Portfolio({
        '動能策略': (report1, 0.4),
        '價值策略': (report2, 0.3),
        '營收成長策略': (report3, 0.3)
    })

    # Load existing positions
    pm = PortfolioSyncManager.from_cloud()

    # Update (actual rebalancing only on rebalance dates)
    pm.update(port, total_balance=5000000)

    # Sync to cloud
    pm.to_cloud()

    # To place orders, enable the following code:
    # account = SinopacAccount()
    # pm.sync(account)

    print("每日同步完成!")

# Use cron or a scheduling tool for periodic execution
# e.g., run daily at 8:30 AM

Stage 6: Performance Tracking & Adjustment

6.1 Monitor Live vs Backtest Divergence

# Get target positions
target_position = pm.get_position()

# Display target holdings
print(pm.get_total_position())

# Use pm.sync to simulate orders and check live vs target differences
pm.sync(account, view_only=True)

6.2 Periodic Strategy Weight Reassessment

# Quarterly reassessment
def quarterly_rebalance():
    """
    Quarterly execution:
    1. Reassess each strategy's performance
    2. Adjust weights
    3. Remove underperforming strategies
    4. Add well-performing strategies
    """
    # Get the latest 3-month performance
    sharpe1 = report1.get_stats()['daily_sharpe']
    sharpe2 = report2.get_stats()['daily_sharpe']
    sharpe3 = report3.get_stats()['daily_sharpe']

    print(f"動能策略 3M 夏普率: {sharpe1:.2f}")
    print(f"價值策略 3M 夏普率: {sharpe2:.2f}")
    print(f"營收成長策略 3M 夏普率: {sharpe3:.2f}")

    # Remove strategies with Sharpe < 0.5
    active_strategies = {}
    if sharpe1 >= 0.5:
        active_strategies['動能策略'] = (report1, sharpe1)
    if sharpe2 >= 0.5:
        active_strategies['價值策略'] = (report2, sharpe2)
    if sharpe3 >= 0.5:
        active_strategies['營收成長策略'] = (report3, sharpe3)

    # Reallocate weights
    total_sharpe = sum(s for _, s in active_strategies.values())
    new_port = Portfolio({
        name: (report, sharpe/total_sharpe)
        for name, (report, sharpe) in active_strategies.items()
    })

    print(f"\n保留 {len(active_strategies)} 個策略,重新調整權重")
    return new_port

# Execute quarterly
new_port = quarterly_rebalance()

Complete Code Summary

# =============================================================================
# Multi-Strategy Portfolio Management Complete Example
# =============================================================================

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

# 1. Build strategy pool
close = data.get('price:收盤價')
volume = data.get('price:成交股數')
pb = data.get('price_earning_ratio:股價淨值比')
roe = data.get('fundamental_features:股東權益報酬率')
rev = data.get('monthly_revenue:當月營收')

# Strategy 1: Momentum
position1 = ((close == close.rolling(20).max()) &
             (volume > volume.average(20) * 1.5)).hold_until(
    exit=close < close.average(10),
    stop_loss=0.08
)
report1 = sim(position1, resample='W', name="動能策略", upload=False)

# Strategy 2: Value
position2 = ((pb < pb.quantile(0.3, axis=1)) &
             (roe > 0.1)).hold_until(
    exit=pb > pb.quantile(0.7, axis=1)
)
report2 = sim(position2, resample='M', name="價值策略", upload=False)

# Strategy 3: Revenue Growth
rev_momentum = rev.average(3) / rev.average(12)
position3 = ((rev_momentum > 1.15) &
             (close > close.average(60))).hold_until(
    exit=rev_momentum < 1.0,
    stop_loss=0.1
)
report3 = sim(position3, resample='M', name="營收成長策略", upload=False)

# 2. Build portfolio
port = Portfolio({
    '動能策略': (report1, 0.4),
    '價值策略': (report2, 0.3),
    '營收成長策略': (report3, 0.3)
})

# 3. Evaluate portfolio performance
port.display()
print(port.report.get_stats())

# 4. Live execution
pm = PortfolioSyncManager()
pm.update(port, total_balance=5000000)
print(pm)

# 5. Sync to cloud
pm.to_cloud()

print("多策略組合建立完成!")

Key Takeaways

Strategy Pool Building Stage

  • Select strategies with different styles (technical, fundamental, institutional)
  • Low correlation between strategies for better diversification
  • Each strategy must pass thorough backtest validation

Portfolio Weighting Stage

  • Equal weight is the simplest starting point
  • Sharpe ratio weighting can improve overall risk-adjusted returns
  • Reassess weights periodically (quarterly)

Backtest Validation Stage

  • Portfolio Sharpe ratio should be higher than individual strategies
  • Portfolio max drawdown should be smaller than individual strategies
  • Check holding diversification and strategy overlap

Live Execution Stage

  • Use PortfolioSyncManager for automated position management
  • Sync regularly (daily) to ensure position consistency
  • Monitor live vs backtest divergence

Continuous Optimization Stage

  • Reassess strategy performance quarterly
  • Remove strategies with Sharpe < 0.5
  • Dynamically adjust weights based on market conditions

Frequently Asked Questions

Q1: How many strategies should a portfolio include?

3-5 strategies is recommended. Too few cannot effectively diversify, and too many dilute returns while increasing management complexity.

Q2: How to determine if a strategy should be removed?

Monitor the 3-6 month rolling Sharpe ratio; consider removing if it consistently stays below 0.5.

Q3: Can strategies be added or removed dynamically?

Yes! Use quarterly_rebalance() for periodic evaluation, adding well-performing strategies and removing underperformers.

Q4: How does PortfolioSyncManager handle conflicts?

When multiple strategies hold the same stock, weights are automatically summed. For example, if Strategy A holds 2330 at 5% and Strategy B holds 2330 at 3%, the final portfolio holds 8%.

Q5: How to avoid excessive trading?

  • Use longer resample periods (M or Q)
  • Set minimum rebalancing thresholds (e.g., don't adjust if weight change < 2%)
  • Prefer strategies with lower rebalancing frequency

Reference Resources