-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsizing.py
More file actions
107 lines (87 loc) · 4.55 KB
/
Copy pathsizing.py
File metadata and controls
107 lines (87 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""Shared trade-affordability check.
Applied by the option backtest (``backtest.run_backtest``) so simulated entries
respect the same caps a real account of this size would face: a signal that
would be refused on cost grounds is also skipped in the backtest, keeping
backtested option P&L honest about what an account could actually take.
"""
from __future__ import annotations
from config import AffordabilityParams
def affordability(premium_per_contract: float, contracts: int, equity: float,
cash: float, deployed_premium: float,
caps: AffordabilityParams) -> tuple[bool, str]:
"""Whether buying `contracts` at `premium_per_contract` is within `caps`.
premium_per_contract : option price per share (x100 = one contract's premium)
deployed_premium : dollar premium already tied up in open positions
Returns (ok, reason) -- `reason` is empty when ok.
"""
cost = premium_per_contract * 100 * contracts
if cost > caps.max_premium_per_trade_pct * equity:
return False, (f"premium ${cost:,.0f} exceeds the per-trade cap of "
f"{caps.max_premium_per_trade_pct:.0%} of equity")
if deployed_premium + cost > caps.max_total_premium_pct * equity:
return False, (f"would push open premium past the "
f"{caps.max_total_premium_pct:.0%}-of-equity cap")
if cash - cost < caps.min_cash_buffer:
return False, (f"would drop cash below the ${caps.min_cash_buffer:,.0f} "
f"buffer")
return True, ""
def risk_based_contracts(equity: float, premium_per_contract: float,
risk_per_trade: float, stop_loss_frac: float) -> int:
"""Contracts sized so the premium at risk is ~`risk_per_trade` of equity.
Risk per contract = premium x 100 x `stop_loss_frac` (the slice of premium
assumed lost when the underlying hits its stop). Always returns at least 1 so
a signal is never sized to zero; the affordability cap trims it down to fit.
"""
risk_per_contract = premium_per_contract * 100 * stop_loss_frac
if risk_per_contract <= 0 or risk_per_trade <= 0:
return 1
return max(1, int((equity * risk_per_trade) / risk_per_contract))
def affordable_contracts(premium_per_contract: float, want: int, equity: float,
cash: float, deployed_premium: float,
caps: AffordabilityParams) -> int:
"""Largest count <= `want` that passes `affordability`, or 0 if even 1 fails.
Lets risk-based sizing scale *down* to the caps instead of skipping the trade
outright -- a one-contract entry that is itself unaffordable still returns 0.
"""
for qty in range(max(want, 0), 0, -1):
ok, _ = affordability(premium_per_contract, qty, equity, cash,
deployed_premium, caps)
if ok:
return qty
return 0
def should_flatten(drawdown: float, flatten_level: float) -> bool:
"""Whether the hard drawdown stop has tripped (close everything).
`flatten_level <= 0` disables it. This is the second, deeper tier above the
entry halt: the halt stops *opening* risk, this closes *existing* risk.
"""
return flatten_level > 0 and drawdown >= flatten_level
def regime_scale(atr_pct: float, lo: float, hi: float, floor: float) -> float:
"""Position-size multiplier in [floor, 1]: full in the regime band's core,
tapering linearly to `floor` at its edges.
`floor >= 1` (or no band configured) disables scaling -> 1.0. A one-sided
band (only `lo` or only `hi`) is widened to a sensible range so there is
still a centre to taper around.
"""
if floor >= 1.0 or (lo <= 0 and hi <= 0):
return 1.0
if hi <= 0:
hi = lo * 3.0
if lo <= 0:
lo = hi / 3.0
half = (hi - lo) / 2.0
if half <= 0:
return 1.0
dist = min(abs(atr_pct - (lo + hi) / 2.0) / half, 1.0) # 0 core .. 1 edge
return floor + (1.0 - floor) * (1.0 - dist)
def drawdown_halt(drawdown: float, halted: bool, halt_level: float,
resume_level: float) -> bool:
"""New halt state for the drawdown circuit-breaker (hysteresis).
`drawdown` is the current fraction below the equity peak. Once it reaches
`halt_level` new entries halt, and stay halted until it recovers to at or
below `resume_level`. `halt_level <= 0` disables the breaker (never halts).
"""
if halt_level <= 0:
return False
if halted:
return drawdown > resume_level # stay halted until recovered
return drawdown >= halt_level # trip at the halt level