昨天處理的是「這張單送不送得出去」。今天處理它前面那個問題:這張單該下多少錢。
Day 19 到 Day 22 的四天回測裡,這個問題有一個沒被檢查過的答案:滿倉。部位是 +1 或 0,也就是「有訊號就把全部資金押上去」。那四天的所有數字都建立在這個預設值上,而它從來沒有被當成一個選項討論過。
今天把它變成一個選項。而選項之間的差距比預期大得多——同一個策略、同一組 232 筆交易,滿倉的總報酬是 −63.54%,固定四分之一倉是 −21.35%。
部位(position) 這個詞在 Day 16 出現過,那時候它只有方向。今天它有大小。
大小的單位是總資金的比例:押 25% 表示一萬鎂的帳戶這一筆下 2,500 鎂。用比例而不是金額,是因為比例會跟著帳戶一起變——賺了之後下得多、虧了之後下得少,而後者是它最有價值的性質。
槓桿(leverage) 是把比例推過 100%:借錢下注,押的錢比帳戶裡的多。現貨市場借不到,所以本系列的主線碰不到它;合約市場可以,而它同時放大獲利與虧損。
爆倉(liquidation) 是槓桿的極限:虧損吃掉保證金,交易所強制平倉,剩下的錢歸零。這件事在現貨不會發生,但它的溫和版本在現貨每天都在發生——帳戶跌到一半的時候,多數人已經關掉機器人了,而那跟爆倉在結果上沒有差別。所以今天量的「破產機率」,定義是「權益跌到起始資金的一半以下」,不是「歸零」。
凱利公式(Kelly criterion) 稍後單獨講,它需要先有前面這些詞。
從最單純的往下走。
固定金額:每一筆都下 1,000 鎂。它是加法的——賺錢時不放大、虧錢時不縮小。
它沒有被做成一個積木,而理由值得寫出來,因為它是一個誠實的限制而不是遺漏:在權重制的回測裡,「固定金額」會是一條隨著權益反向變化的權重序列(帳戶越大、1,000 鎂佔的比例越小),而那個形狀需要知道當下的權益。當下的權益是路徑相依的東西,而 Day 19 選的是向量化回測——它一次算完整段,沒有「當下」這個概念。要表達固定金額得先有一個逐筆模擬的引擎。
固定比例:每一筆都押 25%。它是乘法的,所以它有一個很好的性質——破產在數學上變得不可能。連虧十次之後帳戶還剩 5.6%,永遠不會歸零。
# quantbot/domain/sizing/fixed_fraction_sizer.py
class FixedFractionSizer:
"""每一筆都押總資金的固定比例。實作 domain 的 PositionSizer。
它跟「固定金額」的差別是複利:固定金額(每次都下 1000 鎂)在賺錢時不會放大、
在虧錢時不會縮小,所以它的權益曲線是加法的;固定比例是乘法的,賺了之後下得
多、虧了之後下得少,而那個自動縮小是它最有價值的性質——**它讓破產在數學上
變得不可能**(押 25% 的話,連虧十次還剩 5.6%)。
"""
波動率目標:先決定「這一筆最多輸多少」,再用停損距離反推部位。這是 Day 12 那個 ATR 留下來的伏筆:
# quantbot/domain/sizing/volatility_target_sizer.py
"""先決定「這一筆最多輸多少」,再用停損距離反推部位。實作 PositionSizer。
它是這四個積木裡唯一會隨市場改變下注大小的一個,而它的算式只有一行:
權重 = 每筆風險預算 ÷ 停損距離(以價格比例表示)
停損距離用 Day 12 的 ATR 決定,這是那一天留下來的伏筆:ATR 是價格單位的
波動度量,除以收盤價就變成比例。停損放在 2 個 ATR 之外、每筆願意輸 1% 的話,
波動小的時候(ATR 是價格的 0.25%)停損距離 0.5%,權重 200%(會被上限夾到
25%);波動大的時候(ATR 是價格的 2%)停損距離 4%,權重 25%。
**同一個風險預算,在不同的市況下對應完全不同的部位大小。** 這是「風險平價」
的雛形,也是它跟固定比例最大的差別:固定比例在暴漲暴跌的那幾天押的錢跟
在無風無雨的那幾天一樣多。
"""
停損距離用 ATR 而不是固定百分比,是因為固定百分比兩頭都不對:低波動期會被頻繁掃到(價格正常呼吸就碰到停損),高波動期又形同沒有停損。
這個積木有一個必須明講的行為:ATR 算不出來的那幾根不下注。
# quantbot/domain/sizing/volatility_target_sizer.py
"""NEVER 在 ATR 是 NaN 的那幾根硬給一個權重。暖機期的 ATR 沒有定義,
而「算不出波動就不下注」是唯一安全的選項,用預設值代替會在最沒有資訊的
時候下最大的注。
"""
ATR 為 0 也算「算不出來」而不是「風險為零」。後者會讓權重變成無限大,而那個除以零在測試裡是一條斷言。
凱利公式回答的是:知道勝率與賠率的情況下,押多少能讓長期複利成長率最大。
f* = p − (1 − p) / b
p 是勝率,b 是賠率(平均獲利除以平均虧損)。勝率 60%、賠率 2 的話,f* = 0.6 − 0.4 / 2 = 0.4,也就是押四成。
它最大化的是長期複利成長率,不是期望報酬。這個差別是凱利存在的全部理由:最大化期望報酬的答案永遠是「全押」,而全押在有限次數內破產的機率趨近 1。
然後是那個前提:
# quantbot/domain/sizing/fractional_kelly_sizer.py
"""凱利公式的一個分數。實作 domain 的 PositionSizer。
凱利給的是「最大化長期複利成長率」的下注比例,而它有一個致命的前提:
**它假設我們知道真實的勝率與賠率。** 我們不知道。手上有的是一段歷史算出來的
估計值,而那個估計值的誤差在凱利的算式裡會被放大——高估勝率會讓建議的比例
暴增,而下注過量的懲罰不對稱:押到最佳比例的兩倍,長期成長率會變成負的,
即使那個策略本身是賺錢的。
所以實務上一律用**分數凱利**(1/4 到 1/2)。乘上 0.25 之後,就算真實的最佳
比例只有估計值的一半,我們押的還是低於最佳值——那一側的懲罰溫和得多。
"""
「懲罰不對稱」是分數凱利的整個論證。押不足的代價是成長慢一點;押過量的代價是成長率變成負的。兩邊都不確定的時候,往押不足那一側偏是唯一合理的選擇。
除了乘上一個分數,這個積木還處理兩件事,而它們在實務上比公式本身重要:
樣本不足時退回一個保守的固定比例。 Day 22 那張表上,均值回歸的勝率 53.6% 與賠率 0.64 是 28 筆交易算出來的。28 筆的勝率信賴區間大概有正負 18 個百分點,而那個區間解出來的凱利比例可以從 0 跨到 40%。所以樣本低於門檻時不用凱利。
完全沒有統計時也一樣退回固定比例。 第一次上線的策略手上什麼都沒有,而那正是最需要一個明確行為的時刻。
歷史統計刻意不從回測報告自動生出來:
# quantbot/domain/values/trade_statistics.py
"""一段歷史交易的勝率、賠率與樣本數。凱利公式的三個輸入。
這個值刻意不從回測報告自動生出來(沒有 from_report 之類的建構子)。
理由是「哪一段歷史算出來的統計,可以拿去決定未來的下注比例」是一個要人判斷的
問題——樣本外的、走過完整多空循環的、還是剛好那三個月的?一個方便的建構子會
讓這個判斷消失。
"""
四種下注方式後面是同一個 Protocol,形狀跟 Day 15 的 Feature、Day 16 的 Condition 一樣:
# quantbot/domain/interfaces/position_sizer.py
class PositionSizer(Protocol):
"""一次該下多少:吃一份 SizingContext,回一條與它等長的權重序列。
契約有兩條,兩條都是刻意收得很窄:
- **回傳的權重 NEVER 決定方向。** 方向來自引擎(Day 16),積木只決定大小。
實作照 context.directions 的符號走,一個把方向反過來的積木會讓「策略說做多
但下注模組決定做空」變成合法的程式碼。
- **回傳值不必自己遵守風險上限。** 上限由 RiskLimits 在之後統一套用,
所以積木可以誠實地說出它算出來的數字(凱利算出 3.4 倍就寫 3.4 倍),
而被夾掉的那一段會出現在報告裡。積木自己夾的話那個資訊就消失了。
"""
第二條有一個具體的好處,而它出現在實測的表上:波動率目標在最極端的那一根要求 310.3% 的權重。如果它自己夾成 100%,報告上只會看到 100%,而「這個積木在某些市況下想押三倍」這個資訊就不見了。被夾掉的比例越高,代表那個積木的估計越不可靠,或者風險預算設得太小。
這是今天最重要的設計決定。部位計算是可插拔的積木,所以它是使用者可以自由換的東西;上限不是。
# quantbot/domain/values/risk_limits.py
@dataclass(frozen=True)
class RiskLimits:
"""硬性上限。**它在部位計算之後套用,而且 NEVER 由策略設定檔覆寫。**
這個「之後」是整個 Day 24 最重要的設計決定。部位計算是可插拔的積木,
所以它是使用者可以自由換的東西;而上限不是——一個算出「這一筆下 300% 資金」
的積木(凱利在勝率被高估時很容易算出這種數字)如果能自己決定要不要遵守上限,
那上限就只是一個建議。
落地方式是型別上的:`sizing.yaml` 裡沒有任何欄位對得到這三個值,而多寫一個
`maximum_weight:` 會被 FeatureParameters.ensure_used() 當成拼錯的參數直接
報錯——因為沒有任何 builder 會去取它。這條規則因此不靠人記得。
"""
maximum_weight_per_trade: float = 0.25
maximum_total_weight: float = 1.0
daily_loss_halt_fraction: float = 0.05
最後那段是這個設計最省事的部分:規則不需要一段程式碼去守。Day 15 為了抓拼錯的參數名寫的 ensure_used(),剛好也擋掉了「在設定檔裡偷改風險上限」——因為兩件事在型別上是同一件事:一個沒有任何 builder 會去讀的鍵。
測試把它釘住:
# tests/domain/sizing/test_position_sizer_registry.py
def test_the_risk_ceiling_cannot_be_overridden_from_the_configuration():
"""這是「上限不由設定檔覆寫」的落地方式:沒有 builder 會取這個鍵,
於是 ensure_used() 把它當成拼錯的參數擋下來。"""
with pytest.raises(ValueError, match="maximum_weight_per_trade"):
PositionSizerRegistry().build(
SizingSpecification(
kind="fixed_fraction", parameters={"maximum_weight_per_trade": 3.0}
)
)
夾取本身有一個容易寫錯的地方:
# quantbot/domain/values/risk_limits.py
def clamp(self, weights: pd.Series) -> pd.Series:
"""把權重夾進上限,方向保留。
用絕對值夾再乘回符號,而不是 `clip(-ceiling, ceiling)`:後者在權重是
NaN 的時候會留下 NaN,而 NaN 乘進報酬序列會讓整段權益曲線從那一根起
全部是 NaN——一個「算不出部位」的錯誤會表現成「回測沒有結果」,
而那看起來像資料問題,不像部位計算問題。
算不出部位的那幾根一律當空手。NEVER 用「上次的權重」補,那是憑空
生出一個沒有依據的部位。
"""
Day 19 的回測引擎一行都不用改,而那是 Day 16 把部位定義成權重而不是金額換到的東西:
# quantbot/domain/services/position_sizing_service.py
class PositionSizingService:
"""把引擎給的方向序列(±1/0)換成帶大小的權重序列,並強制套上風險上限。
**第一,回測引擎一行都不用改。** Day 19 的 BacktestService 吃的是一條權重
序列,然後把它乘上價格報酬——它從來沒有假設那條序列只有 0 與 ±1。所以部位
大小接進去的方式是「換掉那條序列」,不是「在回測裡加一段部位邏輯」。
**第二,上限一定會被套用。** 這個 service 是唯一一條產生「可以拿去回測的
權重序列」的路徑,而它在 return 之前一定會呼叫 limits.clamp()。積木自己不夾、
也夾不到——它拿不到 RiskLimits。
"""
成本也自動跟著對:BacktestService 用換手量(|position[t] − position[t-1]|)計價,所以押 25% 的那一筆手續費也是滿倉的四分之一。這件事不需要為部位大小另寫一段。
有了不同的下注比例,接下來的問題是「哪一種比較容易活下來」。這要看的不是一條權益曲線,是一群。
# quantbot/domain/services/equity_simulation_service.py
class EquitySimulationService:
"""把同一組交易的順序打亂幾百次,看不同下注比例的權益曲線分布長什麼樣。
**為什麼打亂順序而不是重新抽樣。** 重抽(bootstrap)會同時改變「哪些交易發生
了」,於是結果的變化裡混了兩個來源:下注比例的影響,與抽樣的影響。打亂順序
只改一個變數——每一條路徑用的是**完全相同的那幾筆交易**,只是先後不同。
這帶來一個很好的性質,而它也是 Day 22 那句「回撤是路徑性質」的直接示範:
在複利之下,同一組報酬不管什麼順序,**最終權益完全相同**(乘法可交換),
但**最大回撤與破產機率會變**。
"""
那個性質有一條測試守著,而它同時是 Day 22 那句話的證明:
# tests/domain/services/test_equity_simulation_service.py
def test_shuffling_never_changes_the_final_equity():
"""複利之下乘法可交換,所以總報酬在所有路徑上是同一個數字。
這正是 Day 22 那句「回撤是路徑性質」的另一半:換順序不變的是報酬,
會變的是路徑。
"""
paths = SERVICE.paths(trades(), weight=1.0, seed=1, path_count=50)
finals = paths.iloc[:, -1]
assert finals.std() == pytest.approx(0.0, abs=1e-12)
所以這份模擬的輸出要這樣讀:總報酬那一欄在所有路徑上是同一個數字,而風險那幾欄不是。 一千條路徑一次算完(path_count × trade_count 的矩陣),「NEVER 用 for loop 遍歷 K 線」在這裡的對應是「NEVER 用 for loop 跑模擬路徑」。
跑的是 Day 17 到 Day 18 那三份設定,BTC/USDT 現貨 1 小時 K 線,資料來自 Day 07 的 TimescaleDB。上限先放寬到 100%,讓四種下注方式的差別看得出來。試驗次數 1(沒有搜尋,四種下注方式是事先寫好的四份設定)。
趨勢跟隨(訊號 232 筆):
| 下注方式 | 交易 | 平均權重 | 要求上限 | 被夾 | 總報酬 | 最大回撤 | 破產機率 |
|---|---|---|---|---|---|---|---|
| 滿倉 | 232 | 100.0% | 100.0% | 0.0% | −63.54% | −66.55% | 100.0% |
| 固定 25% | 232 | 25.0% | 25.0% | 0.0% | −21.35% | −23.07% | 0.0% |
| 波動率目標 1% | 232 | 83.3% | 310.3% | 19.2% | −52.84% | −54.88% | 100.0% |
| 四分之一凱利 | 0 | 0.0% | 0.0% | 0.0% | 0.00% | 0.00% | 0.0% |
均值回歸(訊號 28 筆):
| 下注方式 | 交易 | 平均權重 | 要求上限 | 被夾 | 總報酬 | 最大回撤 | 破產機率 |
|---|---|---|---|---|---|---|---|
| 滿倉 | 28 | 100.0% | 100.0% | 0.0% | −2.45% | −5.64% | 0.0% |
| 固定 25% | 28 | 25.0% | 25.0% | 0.0% | −0.59% | −1.42% | 0.0% |
| 波動率目標 1% | 28 | 84.4% | 188.4% | 0.4% | −3.80% | −5.43% | 0.0% |
| 四分之一凱利 | 28 | 5.0% | 5.0% | 0.0% | −0.12% | −0.29% | 0.0% |
動能爆發(訊號 190 筆):
| 下注方式 | 交易 | 平均權重 | 要求上限 | 被夾 | 總報酬 | 最大回撤 | 破產機率 |
|---|---|---|---|---|---|---|---|
| 滿倉 | 190 | 100.0% | 100.0% | 0.0% | −42.97% | −44.85% | 0.1% |
| 固定 25% | 190 | 25.0% | 25.0% | 0.0% | −12.84% | −13.57% | 0.0% |
| 波動率目標 1% | 190 | 73.4% | 215.3% | 0.9% | −34.87% | −35.63% | 0.0% |
| 四分之一凱利 | 0 | 0.0% | 0.0% | 0.0% | 0.00% | 0.00% | 0.0% |
先講一件必須誠實講的:這三個策略在 Day 22 就已經是賠錢的,而下注比例改變不了方向。 押少一點只是賠少一點,那不是一個發現。所以這三張表要看的不是總報酬那一欄,是後面兩欄。
凱利對兩個策略給出的答案是「不要下注」。 趨勢跟隨與動能爆發的期望值都是負的(Day 22 算過:0.238 × 1.86 − 0.762 = −0.32),而凱利對負期望的最佳解就是 0。那兩列的交易筆數是 0 筆,不是 232 筆。
這件事在實作上撞了一次。原本那份比較報告的 DTO 有一條建構時的檢查:「所有列的交易筆數必須相同」,理由聽起來很直觀——換下注方式不改變訊號。第一次實跑就打掉了它:
ValueError: 所有列必須來自同一組訊號,實得交易筆數 [0, 232]
「押 0」在部位序列上跟「不進場」是同一件事,所以下注方式確實可以改變交易筆數,而那正是它在做的判斷。檢查因此改成 bar_count 相同(同一段資料),而交易筆數變成一個要看的欄位——訊號 232 筆、實際下 0 筆的那個差距是這份報告裡最值得看的一格。
均值回歸那一列走的是退路,不是公式。 它的 5.0% 是 fallback_fraction,因為 28 筆交易低於 100 筆的門檻。這一列跟前面兩列不是同一種東西:它不是「凱利算出來 5%」,是「凱利拒絕回答,所以用了一個保守的預設值」。報告上兩者長得一樣,這也是為什麼積木的 describe() 要把門檻寫出來。
破產機率那一欄有一個 0.1%。 動能爆發滿倉的總報酬是 −42.97%,期末權益 57%,看起來離「跌到一半」還有距離。而一千條打亂順序的路徑裡有一條在中途跌破了 50%——同一組交易,只是先後不同。那 0.1% 就是「回撤是路徑性質」最小的一個實例。
趨勢跟隨滿倉的 100% 則是另一回事:它的期末權益只有 36%,任何順序都會在某個時點跌破一半。這個 100% 不是模擬的結論,是總報酬的必然結果,讀的時候要分清楚。
把單筆上限從 100% 收回預設的 25% 再跑一次趨勢跟隨:
| 下注方式 | 交易 | 平均權重 | 要求上限 | 被夾 | 總報酬 | 最大回撤 |
|---|---|---|---|---|---|---|
| 滿倉 | 232 | 25.0% | 100.0% | 48.4% | −21.35% | −23.07% |
| 固定 25% | 232 | 25.0% | 25.0% | 0.0% | −21.35% | −23.07% |
| 波動率目標 1% | 232 | 25.0% | 310.3% | 48.4% | −21.31% | −23.03% |
三列幾乎是同一列。上限成了主導,而三種下注方式的差別被壓掉了。
這件事有兩種讀法,而兩種都對。從風控的角度,這正是硬性上限該有的效果:不管上面那一層算出什麼,曝險就是不超過那個數字。從研究的角度,它是一個警訊——在這組參數下,「比較四種下注方式」這件事本身沒有意義,因為被比較的東西有 48.4% 的時間是同一個值。
所以要比較下注方式,上限必須鬆到讓它們有差別;而要上線,上限必須緊到讓最壞的積木也不會出事。這是兩個不同的設定,不該用同一份。
# quantbot/infrastructure/charting/plotly_sizing_distribution_renderer.py
class PlotlySizingDistributionRenderer:
"""同一組交易、不同下注比例,幾百條打亂順序的權益曲線。
這張圖要說的事只有一件,而它在數字表上很難看出來:**同一個總報酬,不同的
下注比例會給出完全不同的路徑分布。** 押得重的那一格線會散得很開、下緣會探
得很低;押得輕的那一格是一束細線。
橫軸是交易筆數而不是時間,因為每一條路徑的交易順序都被打亂了,時間軸在這裡
沒有意義。這件事要在軸標題上講清楚,否則讀者會把它讀成一條真的權益曲線。
"""
四格並排,每一格上緣標它的破產機率,虛線標破產線。所有格共用同一條縱軸,否則「散得很開」這件事會被各自的縮放抹掉。
波動率目標需要 atr_14,而 Day 17 到 18 的三份設定沒有宣告它。所以第一次跑的結果是這個:
ValueError: 部位計算 volatility_target_0.01 需要欄位 ['atr_14'],但特徵表裡沒有。
把對應的特徵加進策略設定的 features 清單。
這是 Day 17 那道特徵對帳的同一種檢查,只是換了一個提問方向——那時候問「條件引用的欄位有沒有宣告」,現在問「部位計算需要的欄位有沒有宣告」。兩者都在啟動時就失敗,而不是跑完一輪回測才發現某一列全是 NaN。
修法是三份設定各加一行。但加一個特徵會改變特徵表的暖機期,所以前幾天的數字有可能跟著變,而那件事要驗過才能說:
Day 19 原本 K 線 13,823 根,交易 232 筆,總報酬 −63.54%
加了 atr 之後 K 線 13,823 根,交易 232 筆,總報酬 −63.54%
一樣。原因是 ATR 的暖機期是 14 根,而這份設定裡最長的是 EMA 26;暖機期取最長的那一個,所以加一個更短的不會改變表的長度。另外兩份設定裡最長的是活躍度的 168 根,更是遠遠蓋過 ATR。三份都驗過,交易筆數與曝險與 Day 22 那張表完全一致。
quantbot/
├── domain/
│ ├── values/ TradeStatistics(凱利), RiskLimits, SizingContext,
│ │ SizingSpecification, SizedPositions
│ ├── sizing/ 今天新開的資料夾
│ │ ├── full_notional_sizer.py Day 19-22 的隱含行為,現在是一個選項
│ │ ├── fixed_fraction_sizer.py
│ │ ├── volatility_target_sizer.py 回收 Day 12 的 ATR
│ │ ├── fractional_kelly_sizer.py
│ │ └── position_sizer_registry.py
│ ├── interfaces/ PositionSizer, PositionSizerBuilder
│ ├── services/ position_sizing_service.py
│ │ equity_simulation_service.py
│ └── dto/ ruin_risk_report.py, sizing_comparison_report.py
├── application/analyze_sizing_application.py 今天
├── infrastructure/
│ ├── configuration/sizing.yaml + 載入器 今天
│ ├── reporting/text_sizing_comparison_report_renderer.py 今天
│ └── charting/plotly_sizing_distribution_renderer.py 今天
├── entrypoints/sizing_command.py 今天
└── tests/ 7 個新測試檔案,共 55 條
破產機率是從交易明細算出來的,所以資料短一段結論就會變。跑之前先把管線補完:
uv sync
docker compose -f docker/docker-compose.yml up -d
uv run python -m quantbot.entrypoints.ingest_pipeline_command
# 上限放寬,看四種下注方式的差別
uv run python -m quantbot.entrypoints.sizing_command \
--strategy trend_ema_rsi --timeframe 1h \
--start 2025-01-01 --end 2026-08-01 --max-weight 1.0
# 上限收回 25%,看上限主導的樣子
uv run python -m quantbot.entrypoints.sizing_command \
--strategy trend_ema_rsi --timeframe 1h \
--start 2025-01-01 --end 2026-08-01
換一種下注方式只要改 sizing.yaml 的一行:
# quantbot/infrastructure/configuration/sizing.yaml
sizings:
- kind: volatility_target
label: 波動率目標(每筆風險 1%)
risk_fraction: 0.01
atr_column: atr_14
stop_multiple: 2.0
把 risk_fraction 調成 0.02 再跑一次,會看到平均權重與破產機率一起往上走。這份設定裡寫不進風險上限——試著加一行 maximum_weight_per_trade: 3.0,載入時就會失敗。
八項全過才算完成:
uv run pytest 全綠(547 passed)。uv run mypy quantbot 與 uv run lint-imports 全過(275 檔,3 條契約)。第 6 項是今天最值得留著的一條,因為它是一條不需要任何人記得的規則。
免責聲明:本文為程式與資料工程的技術分享,所有數字皆為歷史資料上的觀察與教學範例,不構成投資建議。文中三個策略在真實成本下都不賺錢,下注比例只改變賠的幅度;這段期間 BTC 下跌 33.71%,所有數字都要放在那個背景下讀。
現在機器人會算訊號、會決定下多少、送單也會自己重試與對帳。它還缺一件事:沒有人知道它在做什麼。
明天處理無人值守系統的第一原則——沒有消息不等於好消息。設計三個層級的通知:心跳(每天一次「我還活著」)、事件(有交易發生)、告警(出事了,需要人介入)。告警疲勞是真的,所以設計時要克制:哪些事真的需要半夜叫人起來(連續虧損超過閾值、部位與交易所不一致),哪些不用(單筆停損)。
順帶把 Telegram Bot 做成雙向的:不只推訊息出去,也收指令進來——查目前部位、暫停交易、緊急平倉。那是這套系統唯一的一道人為閘門。
numpy.random.Generator.permuted 每一列各自獨立打亂,這是一次算完一千條路徑的關鍵 — NumPy documentation
Series.clip 的上下界行為,以及它對 NaN 的處理 — pandas documentation, Series.clip