文件
開啟 Studio

API 參考

finlab.portfolio

本頁內容8

finlab.portfolio 把幾份回測報告依你給的權重組成一個投資組合。組合本身也是一份回測報告,display()、get_stats() 都能直接用;要實盤時,再用 PortfolioSyncManager 把組合換成張數、同步到券商帳戶。

選哪個函式

你想做的事用這個
依固定權重組合多個策略Portfolio()
讓各策略的權重隨時間調整Portfolio.from_weight_function()
回測一籃固定權重的股票或 ETFcreate_multi_asset_report()
載入已上傳到雲端的策略報告create_report_from_cloud()
把組合換成張數並同步到券商帳戶PortfolioSyncManager
檢查是否有策略待停損停利出場portfolio.is_stop_triggered()

什麼時候用投資組合

組合不同類型的策略(動能 + 價值 + 成長)可以分散單一策略失效的風險,通常也能降低最大回撤;權重可以依市場狀況或策略表現調整,實盤時用 PortfolioSyncManager 自動更新持倉。

特性單策略回測 (sim)投資組合 (Portfolio)
適用場景單一策略研究多策略組合管理
輸入position DataFrame多個 Report 物件
權重分配自動等權可自訂各策略權重
風險控制單一策略層級組合層級再平衡
雲端同步不支援支援(PortfolioSyncManager)
複雜度簡單中等

組合多個策略

把 {策略名稱: (回測報告, 權重)} 交給 Portfolio:

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

# 策略 1:動能策略(突破均線)
close = data.get('price:收盤價')
ma20 = close.average(20)
position1 = close > ma20
report1 = sim(position1, resample='M', upload=False)

# 策略 2:價值策略(低股價淨值比)
pb = data.get('price_earning_ratio:股價淨值比')
position2 = pb.is_smallest(100)  # 選最小 100 檔
report2 = sim(position2, resample='M', upload=False)

# 策略 3:營收成長策略
rev_yoy = data.get('monthly_revenue:去年同月增減(%)')
position3 = rev_yoy > 20
report3 = sim(position3, resample='M', upload=False)

# 建立投資組合(權重:40%, 30%, 30%)
portfolio = Portfolio(
    {'動能策略': (report1, 0.4), '價值策略': (report2, 0.3), '營收成長': (report3, 0.3)}
)

# Portfolio 本身就是 Report,可直接使用
portfolio.display()

# 查看組合統計
stats = portfolio.get_stats()
print(f"組合年化報酬: {stats['cagr']:.2%}")
print(f"組合夏普率: {stats['daily_sharpe']:.2f}")
print(f"組合最大回撤: {stats['max_drawdown']:.2%}")

傳入單一份 Report 會當成權重 1.0 的單一策略。權重為 0 的策略會被忽略,但至少要有一個權重不是 0。

權重總和不必是 1。 組合的每日報酬是各策略報酬的加權總和,權重不會被重新縮放,總和不到 1 時剩下的部分留作現金、不計報酬:

# 權重總和 0.9:剩餘 10% 留作現金,不計報酬
portfolio = Portfolio({'策略A': (report1, 0.4), '策略B': (report2, 0.5)})

# 全額投入:權重總和 = 1
portfolio = Portfolio({'策略A': (report1, 0.4), '策略B': (report2, 0.6)})

每份報告只用一個市場。 美股用 data.set_market('us'),美股 ETF 用 'us_fund'。請分別建立股票與 ETF 的報告,再用 Portfolio 組合,不要在同一次 sim() 混用兩種市場的代號。

組合後的幾個屬性這樣計算:

  • Portfolio.position 使用各子策略換股日期的聯集,每個策略延用最近一次換股的目標部位;策略尚未開始前,其權重留作現金。
  • Portfolio.creturn 從所有非零權重策略都開始的那天起算(基準 1),到最早結束的策略為止,因此報酬期間可能比部位歷史短;期間內某策略沒有資料的日子(例如該市場休市)報酬為 0,其他策略照常計入。
  • Portfolio.trades 的 position 是佔整個組合的比例(子策略部位 × 策略權重),資金容量依此計算。
類別

finlab.portfolio.Portfolio

Portfolio(reports, _creturn)

建構 Portfolio 物件。

參數

- reports dict[str, tuple[Report, float]]

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

reports dict[str, tuple[Report, float]] | Report
_creturn pd.Series | None

權重怎麼配

等權配置(適合策略特性相似時):

# 3 個策略各 1/3
portfolio = Portfolio(
    {'策略A': (report1, 1 / 3), '策略B': (report2, 1 / 3), '策略C': (report3, 1 / 3)}
)

風險平價(根據策略波動度反向配置):

# 計算各策略波動度
vol1 = report1.creturn.pct_change().std()
vol2 = report2.creturn.pct_change().std()
vol3 = report3.creturn.pct_change().std()

# 反向權重(波動小的配置多)
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(
    {'策略A': (report1, w1), '策略B': (report2, w2), '策略C': (report3, w3)}
)

目標波動(設定組合目標年化波動率):

# 先建立等權組合
portfolio = Portfolio({'策略A': (report1, 0.5), '策略B': (report2, 0.5)})

# 計算組合年化波動率(以每年 252 個交易日估算)
port_vol = portfolio.creturn.pct_change().std() * 252**0.5

# 調整權重以達成目標年化波動率(如 15%)
target_vol = 0.15
leverage = target_vol / port_vol  # 槓桿倍數

# 重新配置(需確保總權重 <= 1)
if leverage <= 1:
    portfolio = Portfolio(
        {'策略A': (report1, 0.5 * leverage), '策略B': (report2, 0.5 * leverage)}
    )

Portfolio.from_weight_function()

用一段權重歷史組合策略,讓各策略的權重隨時間改變。

import pandas as pd
from finlab.portfolio import Portfolio

def weights():
    # 每一欄是一個策略名稱,每一列是那天起生效的權重
    return pd.DataFrame(
        {'動能策略': [0.5, 0.7], '價值策略': [0.5, 0.3]},
        index=pd.to_datetime(['2020-01-02', '2023-01-03']),
    )

portfolio = Portfolio.from_weight_function(
    {'動能策略': report1, '價值策略': report2}, weights
)
參數型別預設說明
reportsdict[str, Report]必填策略名稱對應回測報告。名稱都要出現在權重表的欄位裡。
weight_function不帶參數的函式必填回傳權重表(pd.DataFrame),每一欄是一個策略名稱。最新一列不能有 NaN。

報酬依權重歷史計算:每一列權重在該日收盤調整(不是交易日就順延到下一個交易日收盤),從之後的交易日開始計入報酬,延用到下一列;最新一列是目前的權重。

portfolio.is_stop_triggered()

任何一個策略有待執行的停損或停利出場時回傳 True。

if portfolio.is_stop_triggered():
    print('有策略要停損停利出場')

回測一籃固定權重的股票

create_multi_asset_report() 把固定權重的一籃股票或 ETF 當成一個策略回測,定期再平衡回原本的權重,回傳 Report。其餘參數交給 backtest.sim():resample 預設 'Q'(每季再平衡),upload 一律是 False。

from finlab.portfolio import create_multi_asset_report

report = create_multi_asset_report({'2330': 0.5, '1101': 0.5}, resample='M')
report.display()
函式

finlab.portfolio.create_multi_asset_report

create_multi_asset_report(stock_list, **kwargs) -> Report

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

Example

from finlab.portfolio import create_multi_asset_report


report = create_multi_asset_report({'2330': 0.5, '1101': 0.5})

參數

stock_list dict

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

kwargs object 預設 {}

回傳

ReportReport

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

載入雲端上的策略

create_report_from_cloud() 讀取存在 FinLab 雲端的策略報告(例如 FinLab Studio 策略頁列出的策略),回傳 CloudReport。它和一般 Report 用法相同,也能放進 Portfolio。strategy_id 是雲端策略的名稱;user_id 預設是目前登入的使用者;market 不指定時,從存檔的報告判斷。

from finlab.portfolio import Portfolio, create_report_from_cloud

report_a = create_report_from_cloud('策略A')
report_b = create_report_from_cloud('策略B')
portfolio = Portfolio({'策略A': (report_a, 0.5), '策略B': (report_b, 0.5)})
函式

finlab.portfolio.create_report_from_cloud

create_report_from_cloud(strategy_id, user_id, market) -> CloudReport

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

參數

user_id str

The user id.

strategy_id str

The

market Market | None

同步到券商帳戶

PortfolioSyncManager 保存投資組合的實盤目標持股:在每個策略的換股日,依你提供的資金把策略權重換成張數;換股日之間處理停損停利出場;狀態可以存在本機或 FinLab 雲端;最後把券商帳戶同步到目標。

每天收盤後、隔日開盤前執行一次:

from finlab.online.sinopac_account import SinopacAccount
from finlab.portfolio import Portfolio, PortfolioSyncManager

account = SinopacAccount()
portfolio = Portfolio({'策略A': (report1, 0.5), '策略B': (report2, 0.5)})

pm = PortfolioSyncManager.from_local()
pm.update(portfolio, total_balance=account.get_total_balance())
pm.to_local()

print(pm.get_position())  # 目標張數
pm.sync(account, view_only=True)  # 預覽;拿掉 view_only 才會下單

第一次執行時,用 PortfolioSyncManager() 建立並 pm.to_local() 存檔,不用 from_local()。total_balance 是這個管理器可用的總資金,由你提供(不會自動讀券商);傳入 account.get_total_balance() 就會跟著帳戶走。update() 各參數的用法見實盤下單(多策略)。

update() 需要重建某個策略,但 total_balance 扣掉其他不更新策略的既有持股後已經沒有資金時,會拋出 finlab.exceptions.PortfolioError,而且這次更新什麼都不改;調高 total_balance 再執行一次即可。

類別

finlab.portfolio.PortfolioSyncManager

PortfolioSyncManager(data, start_with_empty)

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

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

參數

data DataPayload | None
start_with_empty bool 預設 False

方法與屬性

from_local
函式

from_local

from_local(name) -> PortfolioSyncManager

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

參數

path str

本地檔案的路徑。

name str 預設 'default'

回傳

PortfolioSyncManagerPortfolioSyncManager

投資組合類別。

from_cloud
函式

from_cloud

from_cloud(name) -> PortfolioSyncManager

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

參數

path str

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

name str 預設 'default'

回傳

PortfolioSyncManagerPortfolioSyncManager

投資組合類別。

from_path
函式

from_path

from_path(path) -> PortfolioSyncManager

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

參數

path str

本地檔案的路徑。

回傳

PortfolioSyncManagerPortfolioSyncManager

投資組合類別。

to_local
函式

to_local

to_local(name) -> None

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

參數

name str 預設 'default'

回傳

None

None

to_cloud
函式

to_cloud

to_cloud(name) -> requests.Response

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

參數

name str 預設 'default'

回傳

requests.Response

None

to_path
函式

to_path

to_path(path) -> None

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

參數

path str

回傳

None

None

update
函式

update

update(portfolio, total_balance, rebalance_safety_weight, smooth_transition, force_override_difference, custom_position, excluded_stock_ids, excluded_symbols, **kwargs) -> None

設定投資組合的函數。

參數

portfolio Portfolio

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

total_balance float 預設 0

總資產。

rebalance_safety_weight float 預設 0.2

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

smooth_transition bool | None

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

force_override_difference bool 預設 False

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

custom_position Position | dict[str, object] | None

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

excluded_stock_ids list[str] | None

排除的股票代碼列表。

excluded_symbols list[str] | None

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

kwargs object 預設 {}

可能的錯誤

PortfolioError

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

get_strategy_position
函式

get_strategy_position

get_strategy_position(strategy_name, at) -> list[PositionEntryDict]

獲取策略的開倉部位。

參數

strategy_name str

策略名稱。

at str

回傳

dictlist[PositionEntryDict]

開倉部位資訊。

get_position
函式

get_position

get_position(at, market_name) -> Position

獲取持倉資訊。

參數

market_name str

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

at str 預設 'close'

回傳

Position

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

clear
函式

clear

clear() -> None

清除持倉資訊。

回傳

None

None

sync
函式

sync

sync(account, at, consider_margin_as_asset, market_name, **kwargs) -> OrderExecutor

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

參數

account Account

交易帳戶。

consider_margin_as_asset bool 預設 True

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

market_name str | None

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

**kwargs Unpack[CreateOrderKwargs] 預設 {}

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

at str 預設 'close'
create_order_executor
函式

create_order_executor

create_order_executor(account, at, consider_margin_as_asset, market_name, **kwargs) -> OrderExecutor

建立 OrderExecutor(不自動下單)。

參數

account Account

交易帳戶。

consider_margin_as_asset bool 預設 False

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

market_name str | None

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

at str 預設 'close'
kwargs object 預設 {}
get_total_position
函式

get_total_position

get_total_position() -> pd.DataFrame

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

回傳

pd.DataFrame

pd.DataFrame: 持倉資訊的 DataFrame。

常見問題

策略之間持有同一檔股票怎麼辦?

Portfolio 會自動合併:

# 假設兩個策略同時持有 2330
# 策略 A (權重 40%): 2330 佔 20%
# 策略 B (權重 60%): 2330 佔 15%

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

# 組合後 2330 的總權重 = 0.4 * 0.20 + 0.6 * 0.15 = 0.17 (17%)
如何設定最大持股數?

在各策略回測時使用 is_largest() 限制:

# 策略 1: 最多 30 檔
position1 = (close > ma20).is_largest(30)
report1 = sim(position1, resample='M')

# 策略 2: 最多 20 檔
position2 = (pb < 1.5).is_largest(20)
report2 = sim(position2, resample='M')

# 組合後最多約 50 檔(可能有重複持股)
portfolio = Portfolio({'策略1': (report1, 0.5), '策略2': (report2, 0.5)})
如何依近期績效調整權重?
# 根據近期績效調整
def dynamic_weights(reports, window=60):
    """根據近 60 日表現調整權重"""
    recent_returns = []
    for report in reports:
        ret = report.creturn.pct_change().tail(window).mean()  # 近期平均報酬
        recent_returns.append(max(ret, 0))  # 負報酬設為 0

    # 歸一化
    total = sum(recent_returns)
    if total == 0:
        return [1 / len(reports)] * len(reports)  # 等權

    return [r / total for r in recent_returns]

# 使用動態權重
weights = dynamic_weights([report1, report2, report3], window=90)
portfolio = Portfolio(
    {
        '策略1': (report1, weights[0]),
        '策略2': (report2, weights[1]),
        '策略3': (report3, weights[2]),
    }
)

要讓權重在回測期間就跟著變,改用上面的 Portfolio.from_weight_function()。

組合後績效反而變差?

可能原因與檢查方法:

  1. 策略相關性過高:

    # 檢查策略相關性
    returns1 = report1.creturn.pct_change()
    returns2 = report2.creturn.pct_change()
    correlation = returns1.corr(returns2)
    print(f"相關性: {correlation:.2f}")  # > 0.8 表示高度相關
    
    # 解決:選擇相關性低的策略(< 0.5)
  2. 權重配置不當:

    # 檢查各策略夏普率
    print(f"策略 1 夏普: {report1.get_stats()['daily_sharpe']:.2f}")
    print(f"策略 2 夏普: {report2.get_stats()['daily_sharpe']:.2f}")
    
    # 給夏普率高的策略更多權重
  3. 回測期間不一致:

    # 確保所有策略使用相同的回測期間
    from finlab import data
    
    data.truncate_start = '2018-01-01'
    
    report1 = sim(position1, resample='M')
    report2 = sim(position2, resample='M')
    # 確保兩個 report 的起始日期一致

下一步

原始碼在 GitHub。

↑↓ 選擇 Enter 開啟 Esc 關閉