Skip to content

Commit b5517e3

Browse files
Pigbibicodex
andauthored
feat: add replayable volatility delever cooldown state (#416)
Co-authored-by: Codex <noreply@openai.com>
1 parent 26c9b90 commit b5517e3

9 files changed

Lines changed: 415 additions & 11 deletions

.github/workflows/drift-check.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
uses: actions/checkout@v6
3232
with:
3333
repository: QuantStrategyLab/QuantPlatformKit
34-
ref: be45863ad75316e3c993338828da356ddfb42f65
34+
ref: 1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f
3535
path: external/QuantPlatformKit
3636

3737
- name: Set up Python
@@ -215,7 +215,7 @@ jobs:
215215
contents: read
216216
issues: write
217217
id-token: write
218-
uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@be45863ad75316e3c993338828da356ddfb42f65
218+
uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f
219219
with:
220220
strategy_domain: us_equity
221221
caller_event_name: ${{ github.event_name }}
@@ -224,7 +224,7 @@ jobs:
224224
snapshot_checkout_path: external/UsEquitySnapshotPipelines
225225
snapshot_repository_ref: ${{ needs.preflight_backtests.outputs.snapshot_repository_ref }}
226226
ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }}
227-
quant_platform_kit_ref: be45863ad75316e3c993338828da356ddfb42f65
227+
quant_platform_kit_ref: 1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f
228228
lifecycle_preflight_artifact: lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }}
229229
strategy_profile: ${{ inputs.strategy_profile || '' }}
230230
secrets:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Stateful volatility-deleveraging control
2+
3+
`us_equity_strategies.volatility_delever_cooldown` is a pure, reusable rule
4+
for strategies that need to prevent immediate re-entry after a local
5+
volatility-deleveraging event. It can apply to SOXL, TQQQ, TECL, or another
6+
strategy, but it is deliberately not part of a broker, plugin, allocation, or
7+
runtime target.
8+
9+
For a two-session cooldown, a trigger blocks the trigger session plus the next
10+
two effective sessions. A further trigger resets the countdown. Re-entry may
11+
only be considered in the following session, and still needs every existing
12+
strategy/regime/position guard to pass.
13+
14+
The helper refuses malformed state, stale dates, changed cooldown settings, or
15+
ambiguous boolean input. `build_volatility_delever_cooldown_transition` then
16+
wraps the result in QPK's immutable `StrategyRiskStateTransition`, binding it
17+
to the frozen strategy candidate, configuration, account scope, frozen input,
18+
and prior transition.
19+
20+
This is a research and paper-adapter building block only. It does **not** alter
21+
the current SOXL configuration, make an unqualified candidate promotion
22+
eligible, persist state, enable a platform, or submit an order. A future
23+
paper-only platform adapter must use an append-only store and fail closed on a
24+
missing predecessor, duplicate writer, stale source, or divergent frozen input.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ requires-python = ">=3.11"
1111
dependencies = [
1212
"pandas>=2.0",
1313
"pytz>=2024.1",
14-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@be45863ad75316e3c993338828da356ddfb42f65",
14+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f",
1515
]
1616

1717
[tool.setuptools]

qsl.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ bundle = "2026.08.0"
66
requires = [
77
"pandas>=2.0",
88
"pytz>=2024.1",
9-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@be45863ad75316e3c993338828da356ddfb42f65",
9+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f",
1010
]
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""Pure, replayable cooldown state for volatility-deleveraging controls.
2+
3+
This module only calculates strategy state. It neither reads broker state nor
4+
changes an allocation. A platform that wants to use it must persist the
5+
result through QuantPlatformKit's immutable strategy-risk-state contract.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from collections.abc import Mapping
11+
from datetime import date
12+
13+
from quant_platform_kit.common import (
14+
StrategyRiskStateIdentity,
15+
StrategyRiskStateTransition,
16+
build_strategy_risk_state_transition,
17+
)
18+
19+
VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION = "volatility_delever_cooldown.v1"
20+
MAX_VOLATILITY_DELEVER_COOLDOWN_SESSIONS = 252
21+
22+
_STATE_FIELDS = frozenset(
23+
{
24+
"schema_version",
25+
"effective_session",
26+
"cooldown_sessions",
27+
"blocked_sessions_remaining",
28+
"reentry_allowed",
29+
"last_deleveraging_session",
30+
"reason_code",
31+
}
32+
)
33+
_REASON_CODES = frozenset(
34+
{
35+
"no_prior_deleveraging",
36+
"deleveraging_triggered",
37+
"cooldown_active",
38+
"cooldown_elapsed",
39+
}
40+
)
41+
42+
43+
def _effective_session(value: object, *, field_name: str = "effective_session") -> str:
44+
normalized = str(value or "").strip()
45+
try:
46+
return date.fromisoformat(normalized).isoformat()
47+
except ValueError as exc:
48+
raise ValueError(f"{field_name} must be an ISO-8601 date") from exc
49+
50+
51+
def _cooldown_sessions(value: object) -> int:
52+
if isinstance(value, bool):
53+
raise TypeError("cooldown_sessions must be an integer")
54+
try:
55+
normalized = int(value)
56+
except (TypeError, ValueError) as exc:
57+
raise ValueError("cooldown_sessions must be an integer") from exc
58+
if str(value).strip() not in {str(normalized), f"+{normalized}"}:
59+
raise ValueError("cooldown_sessions must be an integer")
60+
if not 0 <= normalized <= MAX_VOLATILITY_DELEVER_COOLDOWN_SESSIONS:
61+
raise ValueError(
62+
f"cooldown_sessions must be between 0 and {MAX_VOLATILITY_DELEVER_COOLDOWN_SESSIONS}"
63+
)
64+
return normalized
65+
66+
67+
def _triggered(value: object) -> bool:
68+
if not isinstance(value, bool):
69+
raise TypeError("deleveraging_triggered must be a boolean")
70+
return value
71+
72+
73+
def _validated_prior_state(value: Mapping[str, object] | None) -> dict[str, object] | None:
74+
if value is None:
75+
return None
76+
if not isinstance(value, Mapping) or set(value) != _STATE_FIELDS:
77+
raise ValueError("previous cooldown state has invalid fields")
78+
if value.get("schema_version") != VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION:
79+
raise ValueError("previous cooldown state has an unsupported schema version")
80+
session = _effective_session(value.get("effective_session"), field_name="previous effective_session")
81+
cooldown_sessions = _cooldown_sessions(value.get("cooldown_sessions"))
82+
remaining = value.get("blocked_sessions_remaining")
83+
if isinstance(remaining, bool) or not isinstance(remaining, int) or not 0 <= remaining <= cooldown_sessions:
84+
raise ValueError("previous blocked_sessions_remaining is invalid")
85+
reentry_allowed = value.get("reentry_allowed")
86+
if not isinstance(reentry_allowed, bool):
87+
raise TypeError("previous reentry_allowed must be a boolean")
88+
last_deleveraging_session = value.get("last_deleveraging_session")
89+
if last_deleveraging_session is not None:
90+
last_deleveraging_session = _effective_session(
91+
last_deleveraging_session,
92+
field_name="previous last_deleveraging_session",
93+
)
94+
if last_deleveraging_session > session:
95+
raise ValueError("previous last_deleveraging_session cannot be after the previous effective_session")
96+
reason_code = value.get("reason_code")
97+
if reason_code not in _REASON_CODES:
98+
raise ValueError("previous cooldown state has an unknown reason_code")
99+
if reentry_allowed and remaining != 0:
100+
raise ValueError("previous cooldown state cannot allow re-entry while sessions remain blocked")
101+
return {
102+
"schema_version": VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION,
103+
"effective_session": session,
104+
"cooldown_sessions": cooldown_sessions,
105+
"blocked_sessions_remaining": remaining,
106+
"reentry_allowed": reentry_allowed,
107+
"last_deleveraging_session": last_deleveraging_session,
108+
"reason_code": reason_code,
109+
}
110+
111+
112+
def advance_volatility_delever_cooldown(
113+
*,
114+
previous_state: Mapping[str, object] | None,
115+
effective_session: object,
116+
cooldown_sessions: object,
117+
deleveraging_triggered: object,
118+
) -> dict[str, object]:
119+
"""Calculate the next re-entry state from one frozen session input.
120+
121+
A trigger blocks re-entry in its own session and the configured number of
122+
following sessions. A fresh trigger during a cooldown resets that
123+
cooldown. Input validation is deliberately strict: an absent, malformed,
124+
stale, or differently configured predecessor raises instead of silently
125+
allowing re-entry.
126+
"""
127+
128+
session = _effective_session(effective_session)
129+
configured_cooldown = _cooldown_sessions(cooldown_sessions)
130+
triggered = _triggered(deleveraging_triggered)
131+
prior = _validated_prior_state(previous_state)
132+
if prior is not None:
133+
if configured_cooldown != prior["cooldown_sessions"]:
134+
raise ValueError("cooldown_sessions must not change within a risk-state chain")
135+
if session <= prior["effective_session"]:
136+
raise ValueError("effective_session must advance beyond the previous cooldown state")
137+
138+
if triggered:
139+
return {
140+
"schema_version": VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION,
141+
"effective_session": session,
142+
"cooldown_sessions": configured_cooldown,
143+
"blocked_sessions_remaining": configured_cooldown,
144+
"reentry_allowed": False,
145+
"last_deleveraging_session": session,
146+
"reason_code": "deleveraging_triggered",
147+
}
148+
149+
if prior is None:
150+
return {
151+
"schema_version": VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION,
152+
"effective_session": session,
153+
"cooldown_sessions": configured_cooldown,
154+
"blocked_sessions_remaining": 0,
155+
"reentry_allowed": True,
156+
"last_deleveraging_session": None,
157+
"reason_code": "no_prior_deleveraging",
158+
}
159+
160+
prior_remaining = int(prior["blocked_sessions_remaining"])
161+
if prior_remaining > 0:
162+
return {
163+
"schema_version": VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION,
164+
"effective_session": session,
165+
"cooldown_sessions": configured_cooldown,
166+
"blocked_sessions_remaining": prior_remaining - 1,
167+
"reentry_allowed": False,
168+
"last_deleveraging_session": prior["last_deleveraging_session"],
169+
"reason_code": "cooldown_active",
170+
}
171+
172+
return {
173+
"schema_version": VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION,
174+
"effective_session": session,
175+
"cooldown_sessions": configured_cooldown,
176+
"blocked_sessions_remaining": 0,
177+
"reentry_allowed": True,
178+
"last_deleveraging_session": prior["last_deleveraging_session"],
179+
"reason_code": "cooldown_elapsed",
180+
}
181+
182+
183+
def build_volatility_delever_cooldown_transition(
184+
*,
185+
identity: StrategyRiskStateIdentity | Mapping[str, object],
186+
effective_session: object,
187+
frozen_input_sha256: object,
188+
cooldown_sessions: object,
189+
deleveraging_triggered: object,
190+
previous_transition: StrategyRiskStateTransition | None = None,
191+
) -> StrategyRiskStateTransition:
192+
"""Build an immutable QPK transition around the pure cooldown result.
193+
194+
The caller is responsible for supplying a frozen input digest and for
195+
durably storing the returned transition. This helper does not read or
196+
write a broker, a database, or a runtime configuration.
197+
"""
198+
199+
if previous_transition is not None and not isinstance(previous_transition, StrategyRiskStateTransition):
200+
raise ValueError("previous_transition must be a StrategyRiskStateTransition")
201+
state = advance_volatility_delever_cooldown(
202+
previous_state=previous_transition.state if previous_transition is not None else None,
203+
effective_session=effective_session,
204+
cooldown_sessions=cooldown_sessions,
205+
deleveraging_triggered=deleveraging_triggered,
206+
)
207+
return build_strategy_risk_state_transition(
208+
identity=identity,
209+
effective_session=effective_session,
210+
input_sha256=frozen_input_sha256,
211+
state=state,
212+
previous_transition=previous_transition,
213+
)
214+
215+
216+
__all__ = [
217+
"MAX_VOLATILITY_DELEVER_COOLDOWN_SESSIONS",
218+
"VOLATILITY_DELEVER_COOLDOWN_STATE_SCHEMA_VERSION",
219+
"advance_volatility_delever_cooldown",
220+
"build_volatility_delever_cooldown_transition",
221+
]

tests/test_drift_workflow_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_drift_workflow_wires_real_snapshot_history_and_preflight_bundle() -> No
2121
assert '"conclusion": "success"' in workflow
2222
assert '"QuantStrategyLab/UsEquitySnapshotPipelines"' in workflow
2323
assert "repository: QuantStrategyLab/QuantPlatformKit" in workflow
24-
assert "ref: be45863ad75316e3c993338828da356ddfb42f65" in workflow
24+
assert "ref: 1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f" in workflow
2525
assert "python -m pip install --no-deps -e external/QuantPlatformKit" in workflow
2626
assert "scripts/run_walk_forward_backtest.py" in workflow
2727
assert '"--list-lifecycle-profiles"' in workflow
@@ -30,15 +30,15 @@ def test_drift_workflow_wires_real_snapshot_history_and_preflight_bundle() -> No
3030
assert "LIFECYCLE_PREFLIGHT_BUNDLE_ROOT" in workflow
3131
assert "Upload lifecycle preflight artifact" in workflow
3232
assert "actions/upload-artifact@v7" in workflow
33-
assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@be45863ad75316e3c993338828da356ddfb42f65" in workflow
33+
assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f" in workflow
3434
assert "strategy_domain: us_equity" in workflow
3535
assert "caller_event_name: ${{ github.event_name }}" in workflow
3636
assert "caller_pr_head_repository: ${{ github.event.pull_request.head.repo.full_name || '' }}" in workflow
3737
assert "snapshot_repository: QuantStrategyLab/UsEquitySnapshotPipelines" in workflow
3838
assert "snapshot_checkout_path: external/UsEquitySnapshotPipelines" in workflow
3939
assert "ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }}" in workflow
4040
assert "lifecycle_preflight_artifact: lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }}" in workflow
41-
assert "quant_platform_kit_ref: be45863ad75316e3c993338828da356ddfb42f65" in workflow
41+
assert "quant_platform_kit_ref: 1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f" in workflow
4242
assert "strategy_profile:" in workflow
4343
assert "REQUESTED_STRATEGY_PROFILE: ${{ inputs.strategy_profile || '' }}" in workflow
4444
assert '"--list-profiles"' in workflow

tests/test_qsl_compat_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
ROOT = Path(__file__).resolve().parents[1]
6-
QPK_REVISION = "be45863ad75316e3c993338828da356ddfb42f65"
6+
QPK_REVISION = "1b9c68dcc63be3fcd6ad72ecc20cff9e791d533f"
77
QPK_URL = (
88
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/"
99
f"QuantPlatformKit.git@{QPK_REVISION}"

0 commit comments

Comments
 (0)