Skip to content

Commit 4fee4d9

Browse files
authored
Merge pull request #302 from QuantStrategyLab/codex/qsl-p3-tqqq-static1-qpk-mandate-019fe21a
feat(risk): freeze TQQQ ETF-only research mandate
2 parents 190b757 + d687678 commit 4fee4d9

5 files changed

Lines changed: 751 additions & 6 deletions

File tree

src/quant_platform_kit/position_sizing.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ class KellyResult:
2020

2121

2222
_APPROVED_BOOTSTRAP_MANDATE = "bootstrap_small_account_v2"
23+
_TQQQ_ETF_ONLY_RESEARCH_MANDATE = "tqqq_etf_only_research_v1"
2324
_BOOTSTRAP_LOSS_BUDGET_CAP = 0.01
2425
_BOOTSTRAP_EFFECTIVE_EXPOSURE_CAP = 0.50
2526
_BOOTSTRAP_NOMINAL_CAPS = {1: 0.50, 2: 0.25, 3: 0.15}
27+
_TQQQ_ETF_ONLY_PRODUCTS = {
28+
"TQQQ": (3, 0.15, 0.45),
29+
"BOXX": (1, 0.50, 0.50),
30+
}
2631

2732

2833
def _weight_mapping(value: object, *, allow_empty: bool) -> dict[str, float] | None:
@@ -140,6 +145,7 @@ def validate_reduce_only_normalization(
140145
product_leverage_factors: Mapping[str, int],
141146
effective_exposure_cap: float,
142147
observed_effective_exposure: float,
148+
cash_only: bool = False,
143149
) -> bool:
144150
"""Validate one explicit transition from an over-cap origin toward cash."""
145151
origin = _weight_mapping(origin_weights, allow_empty=False)
@@ -153,6 +159,7 @@ def validate_reduce_only_normalization(
153159
or not isinstance(effective_exposure_cap, (int, float))
154160
or isinstance(observed_effective_exposure, bool)
155161
or not isinstance(observed_effective_exposure, (int, float))
162+
or not isinstance(cash_only, bool)
156163
):
157164
return False
158165
cap = float(effective_exposure_cap)
@@ -178,6 +185,8 @@ def validate_reduce_only_normalization(
178185
target_active = {symbol for symbol, weight in target.items() if weight > 0.0}
179186
if not origin_active or not target_active.issubset(origin_active):
180187
return False
188+
if cash_only and target_active:
189+
return False
181190
if any(target.get(symbol, 0.0) > origin[symbol] + 1e-9 for symbol in origin):
182191
return False
183192
if any(
@@ -199,6 +208,7 @@ def validate_reduce_only_normalization(
199208
def risk_budgeted_target_weight(
200209
*,
201210
risk_mandate_id: str | None = None,
211+
product_symbol: str | None = None,
202212
account_equity: float | None = None,
203213
risk_fraction: float | None = None,
204214
stop_loss_distance: float | None = None,
@@ -209,9 +219,9 @@ def risk_budgeted_target_weight(
209219
) -> float:
210220
"""Return a fail-closed single-account target weight.
211221
212-
``bootstrap_small_account_v2`` permits one ETF position with its approved
213-
product cap. Without that mandate, the legacy 10% and unlevered fallback
214-
applies. This helper does not allocate across strategies.
222+
Approved mandates permit one ETF position with their product caps. Without
223+
a mandate, the legacy 10% and unlevered fallback applies. This pure helper
224+
sizes a target; it does not authorize a product or execution.
215225
"""
216226
numeric_inputs = (
217227
account_equity,
@@ -256,18 +266,35 @@ def risk_budgeted_target_weight(
256266
nominal_cap = _BOOTSTRAP_NOMINAL_CAPS.get(product_leverage_factor, 0.0)
257267
if nominal_cap == 0.0:
258268
return 0.0
269+
elif risk_mandate_id == _TQQQ_ETF_ONLY_RESEARCH_MANDATE:
270+
product = _TQQQ_ETF_ONLY_PRODUCTS.get(product_symbol or "")
271+
if (
272+
product is None
273+
or product_leverage_factor != product[0]
274+
or risk_fraction != _BOOTSTRAP_LOSS_BUDGET_CAP
275+
or stop_loss_distance != 0.05
276+
or drawdown_scalar not in {0.0, 0.5, 1.0}
277+
):
278+
return 0.0
279+
nominal_cap = product[1]
280+
product_effective_cap = product[2]
259281
else:
260282
return 0.0
261283

262284
risk_weight = risk_fraction * drawdown_scalar / stop_loss_distance
263285
if not math.isfinite(risk_weight):
264286
return 0.0
265287

288+
effective_cap = (
289+
product_effective_cap
290+
if risk_mandate_id == _TQQQ_ETF_ONLY_RESEARCH_MANDATE
291+
else _BOOTSTRAP_EFFECTIVE_EXPOSURE_CAP
292+
)
266293
return min(
267294
risk_weight,
268295
nominal_cap,
269296
available_account_exposure,
270-
_BOOTSTRAP_EFFECTIVE_EXPOSURE_CAP / product_leverage_factor,
297+
effective_cap / product_leverage_factor,
271298
)
272299

273300

src/quant_platform_kit/risk/contracts.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,14 @@ class RiskGateAssessment:
222222
proposed_effective_exposure: float | None
223223
outcome: str
224224
reason_codes: tuple[str, ...]
225+
execution_authorized: bool = False
226+
stop_loss_distance: float | None = None
227+
stop_intent_ready: bool | None = None
228+
strategy_breaker_triggered: bool | None = None
229+
account_breaker_triggered: bool | None = None
230+
account_drawdown_fraction: float | None = None
231+
drawdown_scalar: float | None = None
232+
risk_control_state_digest_sha256: str | None = None
225233
assessment_sha256: str = field(init=False)
226234

227235
def __post_init__(self) -> None:
@@ -245,6 +253,14 @@ def __post_init__(self) -> None:
245253
"proposed_effective_exposure": self.proposed_effective_exposure,
246254
"outcome": self.outcome,
247255
"reason_codes": self.reason_codes,
256+
"execution_authorized": self.execution_authorized,
257+
"stop_loss_distance": self.stop_loss_distance,
258+
"stop_intent_ready": self.stop_intent_ready,
259+
"strategy_breaker_triggered": self.strategy_breaker_triggered,
260+
"account_breaker_triggered": self.account_breaker_triggered,
261+
"account_drawdown_fraction": self.account_drawdown_fraction,
262+
"drawdown_scalar": self.drawdown_scalar,
263+
"risk_control_state_digest_sha256": self.risk_control_state_digest_sha256,
248264
}
249265
encoded = json.dumps(
250266
payload,

0 commit comments

Comments
 (0)