風控:活下來比賺錢重要
量化交易的最大諷刺在於:你寫了一個自動化系統,本來是要幫你紀律操作,結果它以機器的速度、完全不帶情緒地執行一個有缺陷的策略——直到你的帳戶見底為止。人工交易者至少還會在損失到某個程度時,手動停下來。程式不會。
第 3 課我們講過過擬合、第 4 課跑過回測、第 5 課確立了 AI 只放在訊號層。但如果你問我,這門課裡哪一堂最重要,我的答案是這堂。因為再好的策略,沒有風控就像跑在沒有護欄的山路:晴天感覺很爽、轉彎見底。
這堂學什麼
- 倉位管理的兩種主流做法:固定比例 vs 保守 Kelly Criterion,各自的優缺點與適用場景
- 停損三種設計:固定百分比、移動停損(Trailing Stop)、ATR 動態停損——什麼情況用哪種
- 最大回撤熔斷:帳戶跌超過 X% 就全面停止,給自己一道最後防線
- 槓桿的數學陷阱:為什麼 3 倍槓桿不等於 3 倍獲利,卻等於爆倉機率暴增
- 資金管理鐵律清單:10 條不容商量的底線
- freqtrade 風控參數完整實作:stoploss、trailing stop、max_open_trades、tradable_balance_ratio
- 實戰對比:相同策略有風控 vs 無風控的回測差距
觀念一:倉位管理——你每筆交易押多少?
倉位管理(Position Sizing)的核心問題很簡單:帳戶裡有 10,000 USDT,每筆交易押多少?
這個問題比「要不要停損」更根本。就算你的策略勝率 60%,如果每次輸都押大、每次贏都押小,還是會破產。

固定比例法(Fixed Fraction)
最直觀的做法:每筆交易固定押帳戶的 N%。
# 計算每筆交易的押注金額
def calc_stake(account_balance: float, fraction: float = 0.02) -> float:
"""
account_balance: 帳戶目前餘額 (USDT)
fraction: 每筆押帳戶的比例,建議 0.01~0.02(1%~2%)
"""
return account_balance * fraction
# 範例
balance = 10_000 # USDT
stake = calc_stake(balance, 0.02)
print(f"每筆下單金額: {stake} USDT") # 200.0 USDT
優點:帳戶縮水時,每筆押注自動跟著縮小,不會因為連輸幾筆就越押越大試圖「回本」。 缺點:參數 N 怎麼選?感覺派。這時候就要用 Kelly。
Kelly Criterion(保守用法)
Kelly 公式給你一個「理論上最佳押注比例」的計算方式:
f* = (p × b - q) / b
其中:
p = 勝率(win rate),例如 0.55
q = 敗率(loss rate),= 1 - p,例如 0.45
b = 盈虧比(每次獲利 / 每次虧損),例如平均贏 1.5%、輸 1% → b = 1.5
def kelly_fraction(win_rate: float, avg_win: float, avg_loss: float) -> float:
"""
win_rate: 勝率,例如 0.55
avg_win: 平均獲利比例,例如 0.015(1.5%)
avg_loss: 平均虧損比例(正數),例如 0.010(1.0%)
回傳:Kelly 建議押注比例
"""
p = win_rate
q = 1 - win_rate
b = avg_win / avg_loss
f = (p * b - q) / b
return max(f, 0) # Kelly 為負代表不應下注
# 範例:勝率 55%、平均贏 1.5%、平均輸 1%
k = kelly_fraction(0.55, 0.015, 0.010)
print(f"Full Kelly: {k:.2%}") # 25.00% — 太激進!
print(f"Half Kelly: {k/2:.2%}") # 12.50%
print(f"Quarter Kelly: {k/4:.2%}") # 6.25% — 較保守
重要:永遠用「半 Kelly」甚至「四分之一 Kelly」。Full Kelly 在理論上最大化長期成長率,但前提是你對 p 和 b 的估計 100% 準確——而你的估計一定有誤差,過度押注會讓你在短期震盪中爆倉。實務上,我的建議是:先用 Kelly 算出參考值,然後取四分之一,再和「帳戶 2% 上限」取小的那個。
觀念二:停損設計——虧到哪裡停?
停損的意義不是「避免虧損」,而是「把每次虧損控制在可接受範圍內」。沒有停損的策略,理論上單筆虧損可以達到 -100%。

固定百分比停損
最簡單。進場後,價格跌超過 X% 就出場。
# freqtrade config.json 設定
{
"stoploss": -0.05 # 進場後跌 5% 停損出場
}
問題:5% 停損對低波動標的可能是合理的,對高波動標的(BTC 日常波動 3-5%)可能一天內被洗出三次。
移動停損(Trailing Stop)
停損線跟著最高點往上移動,但不跟著下跌。當行情回落超過 X% 時才出場——同時保護了下檔,也讓獲利有空間繼續跑。
{
"stoploss": -0.10,
"trailing_stop": true,
"trailing_stop_positive": 0.02,
"trailing_stop_positive_offset": 0.04,
"trailing_only_offset_is_reached": true
}
這組設定的邏輯:
- 進場後最多跌 10% 才停損(保護初始進場)
- 只要行情比進場價高出 4%(
trailing_stop_positive_offset: 0.04),就切換成移動停損模式 - 切換後,停損線設在最高點以下 2%(
trailing_stop_positive: 0.02)——此時每筆交易至少保住 2% 獲利
ATR 動態停損
ATR(Average True Range,平均真實波動幅度)是技術分析裡衡量市場波動的指標。停損距離設為 N 倍 ATR,讓停損自動適應市場環境。
# 在策略的 populate_indicators 方法中加入 ATR
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 計算 14 根 K 線的 ATR
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
# 停損設為 2 倍 ATR(相對進場價)
dataframe['atr_stoploss'] = dataframe['close'] - (2 * dataframe['atr'])
return dataframe
# 搭配 custom_stoploss callback
def custom_stoploss(self, current_time, current_rate, current_profit, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(kwargs['pair'], self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
atr = last_candle['atr']
# 停損設為當前價格下方 2 倍 ATR
stoploss_price = current_rate - (2 * atr)
stoploss_from_open = (stoploss_price / kwargs['trade'].open_rate) - 1
return max(stoploss_from_open, -0.20) # 最多跌 20% 強制停損
觀念三:最大回撤熔斷——帳戶的保險絲
即使每筆交易都有停損,連續虧損仍會把帳戶侵蝕到你心理無法承受的程度。最大回撤熔斷(Max Drawdown Circuit Breaker)的概念是:當整個帳戶從最高點跌超過 X%,就暫停所有新交易,強迫你停下來檢查策略。

# freqtrade 2024+ 版本支援 max_drawdown 設定
# 在 config.json 中加入:
{
"max_open_trades": 3,
"tradable_balance_ratio": 0.99,
# Protections 區塊:熔斷機制
"protections": [
{
"method": "MaxDrawdown",
"lookback_period_candles": 48, # 回看最近 48 根 K 線(1h 時間框架 = 2 天)
"trade_limit": 1, # 期間內至少 1 筆交易才觸發判斷
"stop_duration_candles": 12, # 觸發後暫停 12 根 K 線(12 小時)
"max_allowed_drawdown": 0.10 # 2 天內回撤超過 10% 就暫停
},
{
"method": "StoplossGuard",
"lookback_period_candles": 24,
"trade_limit": 3,
"stop_duration_candles": 6,
"only_per_pair": false # 整體帳戶停損連爆 3 次就暫停
}
]
}
StoplossGuard 是另一個常用的保護機制:在過去 N 根 K 線內,如果連續觸發停損超過 X 次,自動暫停進場。這讓策略在市場環境急劇轉變(例如市場 Regime 切換)時不至於一直虧損、停損、再進場、再虧損。
觀念四:槓桿——散戶墳場
先看一道數學題:
假設你用 3 倍槓桿做多 BTC。
BTC 下跌 10%,你的帳戶虧損 30%。
問:你需要接下來漲多少,才能回到原點?
答案:回本所需報酬 = 30% / (1 - 30%) = 42.9%

這就是槓桿最核心的陷阱:虧損和獲利在數學上是不對稱的。跌 50% 需要漲 100% 才能回本。用 3 倍槓桿時,市場只需要跌 33% 你的倉位就歸零——而幣圈 BTC 在 2022 年熊市跌了超過 70%。
對散戶量化系統,我的建議是:在你對自己的策略有至少 6 個月的實盤(或模擬盤)紀錄之前,不要碰任何槓桿。 槓桿是一個在你策略本身就有 bug 的時候把你加速送走的機制,不是加速獲利的快捷鍵。
資金管理鐵律清單
以下 10 條不是建議,是在你修改策略之前,先要確認都有做到的底線:
1. 每筆交易風險上限:帳戶的 1-2%,不超過 3%
2. 最大同時持倉數:3-5 個幣對,超過分散效果遞減
3. 所有策略都必須有明確的停損位,沒有「只是暫時虧損,再等等」
4. 停損設定後不得在虧損期間手動下調(稱為「moving the stop」,百分之百是情緒決策)
5. 最大回撤熔斷:帳戶從最高點跌超過 15% 就停機審查,不得讓策略繼續跑
6. 不使用槓桿,直到你有 6 個月以上的模擬盤紀錄且績效穩定
7. 不因為前幾筆賺錢就倉促放大倉位(稱為「過度自信偏誤」)
8. 不因為前幾筆虧損就「加碼攤平」(量化策略加碼攤平是直接違反停損邏輯)
9. 定期(每月)審查策略:Sharpe Ratio 降到 0.5 以下或勝率連續下滑需暫停重新評估
10. 永遠有「我可以接受這全部歸零」的心理準備——否則就是賭了超出你承受能力的金額
手把手實戰:給策略加完整風控
我們用第 4 課的 MyFirstStrategy 為基礎,把上面所有觀念轉成 freqtrade 設定,然後用回測對比「有風控 vs 無風控」的差距。
Step 1:設定基本倉位管理
開啟 config.json,修改以下幾個參數:
{
"max_open_trades": 3,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.3,
"amend_last_stake_amount": true,
"dry_run": true,
"dry_run_wallet": 1000
}
這組設定的意義:
max_open_trades: 3:最多同時持有 3 個倉位stake_amount: "unlimited":讓 freqtrade 自動根據帳戶餘額分配倉位(而不是固定金額)tradable_balance_ratio: 0.3:每次只動用帳戶的 30% 餘額(三個倉位各 10%,留 70% 備用)amend_last_stake_amount: true:最後一筆若餘額不足,自動調整到可用金額
這等同於「最多下注帳戶 10% 到單一標的」,接近固定比例法的安全範圍。
Step 2:在策略裡設定停損
建立一個帶有移動停損的策略版本。在策略 Python 檔頂部的類別屬性加入:
class MyFirstStrategyWithRisk(IStrategy):
"""
在 MyFirstStrategy 基礎上加入完整風控
"""
# ── 停損設定 ──
stoploss = -0.05 # 固定停損:跌超過 5% 停損(保底)
trailing_stop = True # 啟用移動停損
trailing_stop_positive = 0.02 # 移動停損幅度:最高點回撤 2%
trailing_stop_positive_offset = 0.04 # 進場後至少漲 4% 才切換為移動停損
trailing_only_offset_is_reached = True # 未達 offset 前用固定 stoploss
# ── 時間框架與其他基本設定(與原策略一致)──
timeframe = '1h'
can_short = False
# ── 最短持倉時間:避免被短暫假突破觸發即停損 ──
# (需 freqtrade 2024.4+)
minimal_roi = {
"0": 0.10, # 立刻進場若漲 10% 就出場
"60": 0.05, # 持倉 60 分鐘後,漲 5% 就出場
"120": 0.01, # 持倉 2 小時後,漲 1% 就出場
"480": 0 # 持倉 8 小時後,任何獲利都出場
}
解釋移動停損的連動邏輯:
進場價: 100 USDT
情境 A:行情立刻跌
→ 跌到 95 USDT(-5%):固定停損觸發,出場
→ 最多虧 5%
情境 B:行情先漲再跌
→ 漲到 104 USDT(+4%):offset 達到,切換為移動停損
→ 此後停損線設在最高點的 -2%
→ 如果最高點是 110 USDT,停損線在 107.8 USDT
→ 行情從 110 跌到 107.8:停損出場,鎖住 +7.8% 獲利
Step 3:加入回撤熔斷保護
在 config.json 中加入 protections 區塊:
{
"protections": [
{
"method": "MaxDrawdown",
"lookback_period_candles": 48,
"trade_limit": 2,
"stop_duration_candles": 24,
"max_allowed_drawdown": 0.08
},
{
"method": "StoplossGuard",
"lookback_period_candles": 24,
"trade_limit": 3,
"stop_duration_candles": 12,
"only_per_pair": false
},
{
"method": "LowProfitPairs",
"lookback_period_candles": 168,
"trade_limit": 2,
"stop_duration_candles": 24,
"required_profit": 0.0
}
]
}
三個保護層:
MaxDrawdown:48 小時內回撤超過 8%,暫停 24 小時StoplossGuard:24 小時內觸發停損 3 次以上,暫停 12 小時(防止在震盪市場反覆被洗)LowProfitPairs:過去一週該幣對交易 2 次以上且全部虧損,暫停做這個幣對 24 小時
Step 4:對比回測——有風控 vs 無風控
先跑無風控版本(原始 MyFirstStrategy,stoploss 極寬、無 trailing、無 protections):
# 無風控版本:stoploss 設成 -0.99(幾乎不停損)
freqtrade backtesting \
--config config.json \
--strategy MyFirstStrategy \
--timerange 20240101-20250101 \
--fees 0.001 \
--breakdown day
# 有風控版本
freqtrade backtesting \
--config config_risk.json \
--strategy MyFirstStrategyWithRisk \
--timerange 20240101-20250101 \
--fees 0.001 \
--breakdown day
你會看到類似這樣的對比(以 BTC/USDT 2024 年回測為例,數字僅供說明):
──────────────── 無風控版本 ────────────────
Final balance: 943.2 USDT (初始 1,000)
Absolute profit: -56.8 USDT (-5.7%)
Max Drawdown: -28.4%
Sharpe Ratio: -0.18
Win Rate: 48.3%
Total trades: 112
──────────────── 有風控版本 ────────────────
Final balance: 1,021.5 USDT (初始 1,000)
Absolute profit: +21.5 USDT (+2.15%)
Max Drawdown: -9.2%
Sharpe Ratio: 0.51
Win Rate: 51.7%
Total trades: 74(trades 減少:保護機制暫停了部分進場)
注意幾個重點差異:
- 最大回撤砍了近 2/3(-28.4% → -9.2%):這代表在最糟的時期,你的帳戶「最多損失」大幅下降
- Sharpe Ratio 從負值變正:承擔單位風險換來的報酬改善
- 交易次數減少:保護機制在震盪期讓策略停手,減少了「反覆被洗、交手續費」的損耗
- 總報酬雖然只有 2.15%:不是什麼驚人的數字,但和 Buy & Hold 比起來承擔的風險小得多
Step 5:用 freqtrade 的繪圖功能視覺化確認
# 生成回測圖表(需要安裝 plotly:pip install plotly)
freqtrade plot-profit \
--config config_risk.json \
--strategy MyFirstStrategyWithRisk \
--timerange 20240101-20250101
# 開啟後在瀏覽器看互動圖
圖表會顯示:帳戶餘額曲線、每筆進出場標記、停損線位置。把「有風控」和「無風控」的圖並排看,就能直觀感受風控的價值——不是讓你賺更多,而是讓回撤曲線「沒那麼恐怖」。

常見坑
坑 1:stoploss 設太小被反覆洗出——Stoploss order closed. Profit: -1.05%
症狀:回測顯示一大堆「Stoploss order closed. Profit: -1.05%」的出場紀錄,勝率極低,總手續費吃掉一大截獲利。
原因:停損設定沒有考慮時間框架的正常波動幅度。在 1 小時 K 線策略裡設 stoploss: -0.01(1%),BTC 的正常 1 小時波動就可能超過 1%,導致你每次進場都立刻被停出。
解法:停損的合理範圍需要參考 ATR。一個粗略的經驗法則是:停損幅度至少要大於「你的時間框架平均 ATR 的 1.5 倍」。可以先跑一個分析:
import pandas as pd
import freqtrade.vendor.qtpylib.indicators as qtpylib
# 在回測之前先看看 ATR 的分布
def analyze_atr(dataframe: pd.DataFrame) -> None:
import ta
dataframe['atr'] = ta.volatility.average_true_range(
dataframe['high'], dataframe['low'], dataframe['close'], window=14
)
dataframe['atr_pct'] = dataframe['atr'] / dataframe['close']
print(f"ATR% 平均值: {dataframe['atr_pct'].mean():.3%}")
print(f"ATR% 95th 分位: {dataframe['atr_pct'].quantile(0.95):.3%}")
print(f"建議最小停損: {dataframe['atr_pct'].mean() * 1.5:.3%}")
坑 2:protections 設定後 dry run 一直顯示「Protection triggered, not opening trade`
症狀:把保護機制設定好,啟動模擬盤後發現策略幾乎不進場,日誌全是:
INFO - Protection triggered! StoplossGuard: 3 stoplosses in 24 candles.
Not opening trade for BTC/USDT.
原因:你的保護機制的 trade_limit 設太低,或 lookback_period_candles 太長。尤其在剛開始跑的時候,如果歷史資料裡有一段震盪行情讓策略連觸幾次停損,保護機制就會鎖住很長時間。
解法:先在回測中觀察「策略在歷史資料裡觸發停損的頻率」,根據這個頻率調整保護機制的閾值。進一步調試時可以用 --timerange 縮短回測區間到你關心的那段時期單獨分析。
# 只看 2024 Q4 那段震盪期
freqtrade backtesting \
--config config_risk.json \
--strategy MyFirstStrategyWithRisk \
--timerange 20241001-20241231 \
--fees 0.001 \
--export trades \
--export-filename backtest_results.json
坑 3:啟用 trailing_stop 後,回測顯示「Exit reason: trailing_stop_loss」但獲利比預期低
症狀:你預期移動停損可以「讓獲利繼續跑」,但看回測發現很多交易都在獲利很小的時候就被移動停損踢出。
原因:可能是 trailing_stop_positive_offset 設太小。如果 offset 只有 1%,市場正常波動就可能在你剛切換移動停損時立刻觸發。
解法:offset 應該設成「你的策略平均持倉期間,市場的正常上漲幅度」至少的 1.5 倍。另外確認設定了 trailing_only_offset_is_reached: true,否則移動停損在你還沒漲到 offset 之前就開始跟蹤,實際上等於把固定停損跟著最低點往下移(保護效果完全相反)。
{
"trailing_stop": true,
"trailing_stop_positive": 0.03,
"trailing_stop_positive_offset": 0.06,
"trailing_only_offset_is_reached": true
}
坑 4:Kelly 公式算出負數,卻還是強行下單
症狀:計算 Kelly 分數時得到負值,卻用絕對值下單——例如 f* = -0.12,誤用為「押 12%」。
原因:Kelly 為負代表「數學上這個策略的期望值為負,根本不應該進場」。負 Kelly 不是「反著押」,而是「這個訊號毫無優勢,任何押注都是在送錢」。
修正:在你的倉位計算函式裡加上保護:
def safe_kelly(win_rate: float, avg_win: float, avg_loss: float,
max_fraction: float = 0.02) -> float:
p = win_rate
q = 1 - win_rate
b = avg_win / avg_loss if avg_loss > 0 else 0
f = (p * b - q) / b if b > 0 else -1
if f <= 0:
# Kelly 為負:策略無優勢,此次不下單
return 0.0
# 取四分之一 Kelly,再取與帳戶上限的最小值
quarter_kelly = f / 4
return min(quarter_kelly, max_fraction)
作業
- 對比實驗:把第 4 課的 MyFirstStrategy 複製一份,按本堂實戰 Step 1–4 加入完整風控,跑同一段時間範圍的回測(含手續費
--fees 0.001),把兩個版本的以下數字記下來:Max Drawdown、Sharpe Ratio、Win Rate、Total Profit。 - ATR 分析:用本堂「坑 1」提供的程式碼,分析你的 BTC/USDT 1 小時資料的 ATR 分布,算出建議的最小停損幅度,確認你的
stoploss設定是否合理。 - Kelly 計算:從你的回測結果取出 Win Rate 和平均盈虧比,用
safe_kelly()函式計算四分之一 Kelly,並和你目前的tradable_balance_ratio / max_open_trades比較——看看你目前的倉位設定相對 Kelly 建議是保守還是激進。 - 選做:壓力測試:把
--timerange設成 2022 年熊市(例如20220101-20221231),用有風控的策略版本跑一次回測,看看在歷史上最惡劣的市場環境中,你的風控是否把回撤控制在可接受範圍。
下一課預告
這堂課解決了「如何不讓程式把帳戶跑光」的問題。下一堂課要回答另一個問題:確認策略和風控都準備好了,怎麼從模擬盤安全地走到真實市場?
第 7 課《模擬盤到實盤:最小可行上線》會帶你做完整的上線清單:API 金鑰安全設定、模擬盤 vs 實盤的指標差異要觀察多久、如何設計一個「最小部位」的真實測試、上線後的監控與警報(包括 freqtrade 的 Telegram 整合),以及你什麼時候應該叫停策略。模擬盤再完美,第一次連上真實市場都會有你沒預期到的問題——下一堂課的目標是讓這些驚喜盡量發生在你損失很小的時候。