API 參考
finlab.portfolio
本頁內容8
finlab.portfolio 把幾份回測報告依你給的權重組成一個投資組合。組合本身也是一份回測報告,display()、get_stats() 都能直接用;要實盤時,再用 PortfolioSyncManager 把組合換成張數、同步到券商帳戶。
選哪個函式
| 你想做的事 | 用這個 |
|---|---|
| 依固定權重組合多個策略 | Portfolio() |
| 讓各策略的權重隨時間調整 | Portfolio.from_weight_function() |
| 回測一籃固定權重的股票或 ETF | create_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 物件。
參數
- reportsdict[str, tuple[Report, float]]代表投資組合的字典,key 為資產名稱,value 是回測報告與部位。
reportsdict[str, tuple[Report, float]] | Report_creturnpd.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
)| 參數 | 型別 | 預設 | 說明 |
|---|---|---|---|
reports | dict[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_listdict一個以股票代號為 key,權重大小為 value。 A dictionary with stock id as key and weight as value.
kwargsobject預設{}
回傳
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_idstrThe user id.
strategy_idstrThe
marketMarket | 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.
參數
dataDataPayload | Nonestart_with_emptybool預設False
方法與屬性
from_local
from_local
from_local(name) -> PortfolioSyncManager從本地檔案初始化投資組合。
參數
pathstr本地檔案的路徑。
namestr預設'default'
回傳
PortfolioSyncManagerPortfolioSyncManager投資組合類別。
from_cloud
from_cloud
from_cloud(name) -> PortfolioSyncManager從雲端檔案初始化投資組合。
參數
pathstr雲端檔案的路徑。預設為 'default'。
namestr預設'default'
回傳
PortfolioSyncManagerPortfolioSyncManager投資組合類別。
from_path
from_path
from_path(path) -> PortfolioSyncManager從本地檔案初始化投資組合。
參數
pathstr本地檔案的路徑。
回傳
PortfolioSyncManagerPortfolioSyncManager投資組合類別。
to_local
to_local
to_local(name) -> None將投資組合資訊存至本地檔案。
參數
namestr預設'default'
回傳
NoneNone
to_cloud
to_cloud
to_cloud(name) -> requests.Response將投資組合資訊存至雲端檔案。
參數
namestr預設'default'
回傳
requests.ResponseNone
to_path
to_path
to_path(path) -> None將投資組合資訊存至本地檔案。
參數
pathstr
回傳
NoneNone
update
update
update(portfolio, total_balance, rebalance_safety_weight, smooth_transition, force_override_difference, custom_position, excluded_stock_ids, excluded_symbols, **kwargs) -> None設定投資組合的函數。
參數
portfolioPortfolio包含投資組合資訊的 Portfolio 或 Report 物件。
total_balancefloat預設0總資產。
rebalance_safety_weightfloat預設0.2換股保留現金比例 (預設 0.2)。
smooth_transitionbool | None是否只在換股日才更新 (None 自動判斷)。
force_override_differencebool預設False是否強制覆蓋不同的部位。
custom_positionPosition | dict[str, object] | None自定義部位 (不列入計算)。
excluded_stock_idslist[str] | None排除的股票代碼列表。
excluded_symbolslist[str] | None排除的股票代碼列表 (同 excluded_stock_ids)。
kwargsobject預設{}
可能的錯誤
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_namestr策略名稱。
atstr
回傳
dictlist[PositionEntryDict]開倉部位資訊。
get_position
get_position
get_position(at, market_name) -> Position獲取持倉資訊。
參數
market_namestr指定市場名稱。預設為 None,也就是獲取所有市場。
atstr預設'close'
回傳
Positiondict or Position: 若 combined 為 True,則返回合併後的持倉資訊(Position 物件); 若 combined 為 False,則返回原始持倉資訊(dict)。
clear
clear
clear() -> None清除持倉資訊。
回傳
NoneNone
sync
sync
sync(account, at, consider_margin_as_asset, market_name, **kwargs) -> OrderExecutor同步持倉資訊並建立委託單。
參數
accountAccount交易帳戶。
consider_margin_as_assetbool預設True是否將保證金交易視為資產。
market_namestr | None指定市場名稱,None 代表所有市場。
**kwargsUnpack[CreateOrderKwargs]預設{}傳遞給
OrderExecutor.create_orders(market_order, best_price_limit, view_only, extra_bid_pct 等)。atstr預設'close'
create_order_executor
create_order_executor
create_order_executor(account, at, consider_margin_as_asset, market_name, **kwargs) -> OrderExecutor建立 OrderExecutor(不自動下單)。
參數
accountAccount交易帳戶。
consider_margin_as_assetbool預設False是否將融資融券視為資產。
market_namestr | None指定市場名稱,None 代表所有市場。
atstr預設'close'kwargsobject預設{}
get_total_position
get_total_position
get_total_position() -> pd.DataFrame回傳目前持倉的 DataFrame(與 repr 顯示的 df 相同)。
回傳
pd.DataFramepd.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()。
組合後績效反而變差?
可能原因與檢查方法:
策略相關性過高:
# 檢查策略相關性 returns1 = report1.creturn.pct_change() returns2 = report2.creturn.pct_change() correlation = returns1.corr(returns2) print(f"相關性: {correlation:.2f}") # > 0.8 表示高度相關 # 解決:選擇相關性低的策略(< 0.5)權重配置不當:
# 檢查各策略夏普率 print(f"策略 1 夏普: {report1.get_stats()['daily_sharpe']:.2f}") print(f"策略 2 夏普: {report2.get_stats()['daily_sharpe']:.2f}") # 給夏普率高的策略更多權重回測期間不一致:
# 確保所有策略使用相同的回測期間 from finlab import data data.truncate_start = '2018-01-01' report1 = sim(position1, resample='M') report2 = sim(position2, resample='M') # 確保兩個 report 的起始日期一致
下一步
原始碼在 GitHub。
這一頁有幫到你嗎?