Skip to content

Commit 0402335

Browse files
Pigbibicodex
andcommitted
feat(risk): add global ETF research mandate contract
Co-Authored-By: Codex <noreply@openai.com>
1 parent 4fee4d9 commit 0402335

2 files changed

Lines changed: 768 additions & 9 deletions

File tree

src/quant_platform_kit/risk/gate.py

Lines changed: 325 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,39 @@
3434
_TQQQ_ETF_ONLY_FACTORS = {"TQQQ": 3, "BOXX": 1}
3535
_TQQQ_ETF_ONLY_NOMINAL_CAPS = {"TQQQ": 0.15, "BOXX": 0.50}
3636
_TQQQ_ETF_ONLY_EFFECTIVE_CAPS = {"TQQQ": 0.45, "BOXX": 0.50}
37+
_GLOBAL_ETF_RESEARCH_MANDATE = "global_etf_rotation_etf_only_research_v1"
38+
_GLOBAL_ETF_STRATEGY_PROFILE = (
39+
"global_etf_rotation_etf_only_single_strategy_research_v1"
40+
)
41+
_GLOBAL_ETF_ACCOUNT_MODE = "single_strategy_research_v1"
42+
_GLOBAL_ETF_ALLOWED_ASSETS = (
43+
"EWY",
44+
"EWT",
45+
"INDA",
46+
"FXI",
47+
"EWJ",
48+
"VGK",
49+
"VOO",
50+
"XLK",
51+
"SMH",
52+
"GLD",
53+
"SLV",
54+
"USO",
55+
"DBA",
56+
"XLE",
57+
"XLF",
58+
"ITA",
59+
"XLP",
60+
"XLU",
61+
"XLV",
62+
"IHI",
63+
"VNQ",
64+
"KRE",
65+
"BIL",
66+
)
67+
_GLOBAL_ETF_FACTORS = {symbol: 1 for symbol in _GLOBAL_ETF_ALLOWED_ASSETS}
68+
_GLOBAL_ETF_CAPS = {symbol: 0.50 for symbol in _GLOBAL_ETF_ALLOWED_ASSETS}
69+
_GLOBAL_ETF_STOP_FILL_POLICY = "gap_aware_min_open_or_stop_v1"
3770
_BOOTSTRAP_EFFECTIVE_EXPOSURE_CAP = 0.50
3871
_BOOTSTRAP_NOMINAL_CAPS = {1: 0.50, 2: 0.25, 3: 0.15}
3972
_ASSESSMENT_CONTRACT_VERSION = "qsl.risk_gate_assessment.v2"
@@ -297,6 +330,97 @@ def _exact_tqqq_mandate_errors(
297330
return {"invalid_tqqq_research_mandate"} if invalid else set()
298331

299332

333+
def _exact_global_etf_mandate_errors(
334+
mandate_provenance: Mapping[str, Any],
335+
*,
336+
effective_at: datetime,
337+
expires_at: datetime,
338+
) -> set[str]:
339+
if mandate_provenance.get("mandate_id") != _GLOBAL_ETF_RESEARCH_MANDATE:
340+
return set()
341+
required = (
342+
"loss_budget_equity_reference",
343+
"product_effective_caps",
344+
"max_nonzero_assets",
345+
"broker_margin_factor",
346+
"margin_stacking",
347+
"borrowing",
348+
"shorting",
349+
"income_sleeve_enabled",
350+
"option_overlay_enabled",
351+
"ai_overlay_enabled",
352+
"market_regime_overlay_enabled",
353+
"precommitted_executable_stop_distance",
354+
"stop_fill_policy",
355+
"max_consecutive_completed_losing_exits",
356+
)
357+
allowed_assets = mandate_provenance.get("allowed_nonzero_assets")
358+
factors = mandate_provenance.get("product_leverage_factors")
359+
exact_factors = (
360+
isinstance(factors, Mapping)
361+
and set(factors) == set(_GLOBAL_ETF_FACTORS)
362+
and all(
363+
not isinstance(factors[symbol], bool)
364+
and isinstance(factors[symbol], int)
365+
and factors[symbol] == expected
366+
for symbol, expected in _GLOBAL_ETF_FACTORS.items()
367+
)
368+
)
369+
invalid = (
370+
any(field not in mandate_provenance for field in required)
371+
or mandate_provenance.get("mandate_version") != "v1"
372+
or mandate_provenance.get("authority_scope") != "RESEARCH_ONLY"
373+
or mandate_provenance.get("strategy_profile")
374+
!= _GLOBAL_ETF_STRATEGY_PROFILE
375+
or mandate_provenance.get("account_mode") != _GLOBAL_ETF_ACCOUNT_MODE
376+
or _finite_number(mandate_provenance.get("max_snapshot_age_seconds"))
377+
!= 300.0
378+
or _finite_number(mandate_provenance.get("effective_exposure_cap")) != 0.50
379+
or _finite_number(mandate_provenance.get("loss_budget")) != 0.01
380+
or mandate_provenance.get("loss_budget_equity_reference")
381+
!= "completed_session_equity"
382+
or not _exact_numeric_mapping(
383+
mandate_provenance.get("product_caps"),
384+
_GLOBAL_ETF_CAPS,
385+
)
386+
or not _exact_numeric_mapping(
387+
mandate_provenance.get("nominal_caps"),
388+
_GLOBAL_ETF_CAPS,
389+
)
390+
or not _exact_numeric_mapping(
391+
mandate_provenance.get("product_effective_caps"),
392+
_GLOBAL_ETF_CAPS,
393+
)
394+
or not exact_factors
395+
or not isinstance(allowed_assets, (list, tuple))
396+
or tuple(allowed_assets) != _GLOBAL_ETF_ALLOWED_ASSETS
397+
or isinstance(mandate_provenance.get("max_nonzero_assets"), bool)
398+
or mandate_provenance.get("max_nonzero_assets") != 2
399+
or isinstance(mandate_provenance.get("broker_margin_factor"), bool)
400+
or mandate_provenance.get("broker_margin_factor") != 1
401+
or mandate_provenance.get("margin_stacking") is not False
402+
or mandate_provenance.get("borrowing") is not False
403+
or mandate_provenance.get("shorting") is not False
404+
or mandate_provenance.get("income_sleeve_enabled") is not False
405+
or mandate_provenance.get("option_overlay_enabled") is not False
406+
or mandate_provenance.get("ai_overlay_enabled") is not False
407+
or mandate_provenance.get("market_regime_overlay_enabled") is not False
408+
or _finite_number(
409+
mandate_provenance.get("precommitted_executable_stop_distance")
410+
)
411+
!= 0.05
412+
or mandate_provenance.get("stop_fill_policy")
413+
!= _GLOBAL_ETF_STOP_FILL_POLICY
414+
or isinstance(
415+
mandate_provenance.get("max_consecutive_completed_losing_exits"),
416+
bool,
417+
)
418+
or mandate_provenance.get("max_consecutive_completed_losing_exits") != 5
419+
or (expires_at - effective_at).total_seconds() > 90 * 24 * 60 * 60
420+
)
421+
return {"invalid_global_etf_research_mandate"} if invalid else set()
422+
423+
300424
def _mandate_fields(
301425
mandate_provenance: Mapping[str, Any] | None,
302426
*,
@@ -392,11 +516,18 @@ def _mandate_fields(
392516
return {}, {"invalid_mandate"}
393517
if effective_at > now or expires_at < now or expires_at <= effective_at:
394518
return {}, {"expired_mandate"}
395-
exact_mandate_errors = _exact_tqqq_mandate_errors(
396-
mandate_provenance,
397-
effective_at=effective_at,
398-
expires_at=expires_at,
399-
)
519+
exact_mandate_errors = set()
520+
for validator in (
521+
_exact_tqqq_mandate_errors,
522+
_exact_global_etf_mandate_errors,
523+
):
524+
exact_mandate_errors.update(
525+
validator(
526+
mandate_provenance,
527+
effective_at=effective_at,
528+
expires_at=expires_at,
529+
)
530+
)
400531
if exact_mandate_errors:
401532
return {}, exact_mandate_errors
402533
factors = mandate_provenance.get("product_leverage_factors", {})
@@ -553,6 +684,13 @@ def _risk_control_fields(
553684
"drawdown_scalar": None,
554685
"risk_control_state_digest_sha256": None,
555686
}
687+
if mandate.get("mandate_id") == _GLOBAL_ETF_RESEARCH_MANDATE:
688+
return _global_etf_risk_control_fields(
689+
risk_control_state,
690+
mandate=mandate,
691+
now=now,
692+
active_positions=active_positions,
693+
)
556694
if mandate.get("mandate_id") != _TQQQ_ETF_ONLY_RESEARCH_MANDATE:
557695
return empty, set()
558696
if not isinstance(risk_control_state, Mapping):
@@ -676,6 +814,165 @@ def _risk_control_fields(
676814
}, errors
677815

678816

817+
def _global_etf_risk_control_fields(
818+
risk_control_state: Mapping[str, Any] | None,
819+
*,
820+
mandate: Mapping[str, Any],
821+
now: datetime,
822+
active_positions: list[tuple[str, float]],
823+
) -> tuple[dict[str, Any], set[str]]:
824+
empty = {
825+
"stop_loss_distance": None,
826+
"stop_intent_ready": None,
827+
"strategy_breaker_triggered": None,
828+
"account_breaker_triggered": None,
829+
"account_drawdown_fraction": None,
830+
"drawdown_scalar": None,
831+
"risk_control_state_digest_sha256": None,
832+
}
833+
if not isinstance(risk_control_state, Mapping):
834+
return empty, {"missing_risk_control_state"}
835+
836+
required = (
837+
"as_of",
838+
"mandate_id",
839+
"candidate_identity_sha256",
840+
"stop_loss_distance",
841+
"stop_fill_policy",
842+
"position_stop_states",
843+
"consecutive_completed_losing_exits",
844+
"account_drawdown_fraction",
845+
"drawdown_scalar",
846+
)
847+
errors: set[str] = set()
848+
if any(field not in risk_control_state for field in required):
849+
errors.add("invalid_risk_control_state")
850+
851+
as_of = _parse_utc_timestamp(risk_control_state.get("as_of"))
852+
stop_loss_distance = _finite_number(risk_control_state.get("stop_loss_distance"))
853+
account_drawdown = _finite_number(
854+
risk_control_state.get("account_drawdown_fraction")
855+
)
856+
drawdown_scalar = _finite_number(risk_control_state.get("drawdown_scalar"))
857+
raw_losses = risk_control_state.get("consecutive_completed_losing_exits")
858+
losses = (
859+
raw_losses
860+
if not isinstance(raw_losses, bool)
861+
and isinstance(raw_losses, int)
862+
and raw_losses >= 0
863+
else None
864+
)
865+
max_age = _finite_number(mandate.get("max_snapshot_age_seconds"))
866+
if as_of is None or max_age is None:
867+
errors.add("invalid_risk_control_state")
868+
elif (age := (now - as_of).total_seconds()) < 0.0 or age > max_age:
869+
errors.add("stale_risk_control_state")
870+
if risk_control_state.get("mandate_id") != mandate.get("mandate_id"):
871+
errors.add("risk_control_mandate_mismatch")
872+
if _sha256(risk_control_state.get("candidate_identity_sha256")) != mandate.get(
873+
"candidate_identity_sha256"
874+
):
875+
errors.add("risk_control_candidate_mismatch")
876+
if stop_loss_distance != 0.05:
877+
errors.add("invalid_stop_loss_distance")
878+
if risk_control_state.get("stop_fill_policy") != _GLOBAL_ETF_STOP_FILL_POLICY:
879+
errors.add("invalid_stop_fill_policy")
880+
if account_drawdown is None or not 0.0 <= account_drawdown <= 1.0:
881+
errors.add("invalid_account_drawdown")
882+
if losses is None:
883+
errors.add("invalid_strategy_breaker_state")
884+
885+
expected_scalar: float | None = None
886+
if account_drawdown is not None and 0.0 <= account_drawdown <= 1.0:
887+
if account_drawdown <= 0.05:
888+
expected_scalar = 1.0
889+
elif account_drawdown <= 0.10:
890+
expected_scalar = 0.50
891+
else:
892+
expected_scalar = 0.0
893+
if drawdown_scalar != expected_scalar:
894+
errors.add("drawdown_scalar_mismatch")
895+
elif drawdown_scalar is None:
896+
errors.add("invalid_drawdown_scalar")
897+
898+
active_symbols = [symbol for symbol, _weight in active_positions]
899+
if len(active_symbols) != len(set(active_symbols)):
900+
errors.add("duplicate_active_symbol")
901+
raw_stop_states = risk_control_state.get("position_stop_states")
902+
normalized_stop_states: dict[str, dict[str, Any]] = {}
903+
all_stop_intents_ready = True
904+
if not isinstance(raw_stop_states, Mapping):
905+
errors.add("invalid_position_stop_states")
906+
all_stop_intents_ready = False
907+
elif set(raw_stop_states) != set(active_symbols):
908+
errors.add("stop_state_positions_mismatch")
909+
all_stop_intents_ready = False
910+
else:
911+
expected_stop_fields = {
912+
"stop_intent_ready",
913+
"entry_fill_identity_sha256",
914+
"stop_entry_fill_identity_sha256",
915+
}
916+
for symbol in sorted(set(active_symbols)):
917+
raw_stop = raw_stop_states.get(symbol)
918+
if not isinstance(raw_stop, Mapping) or set(raw_stop) != expected_stop_fields:
919+
errors.add("invalid_position_stop_state")
920+
all_stop_intents_ready = False
921+
continue
922+
ready = raw_stop.get("stop_intent_ready")
923+
entry_fill_identity = _sha256(
924+
raw_stop.get("entry_fill_identity_sha256")
925+
)
926+
stop_entry_fill_identity = _sha256(
927+
raw_stop.get("stop_entry_fill_identity_sha256")
928+
)
929+
if ready is not True:
930+
errors.add("stop_intent_not_ready")
931+
all_stop_intents_ready = False
932+
if (
933+
entry_fill_identity is None
934+
or stop_entry_fill_identity is None
935+
or entry_fill_identity != stop_entry_fill_identity
936+
):
937+
errors.add("stop_entry_fill_identity_mismatch")
938+
all_stop_intents_ready = False
939+
normalized_stop_states[symbol] = {
940+
"stop_intent_ready": ready if isinstance(ready, bool) else None,
941+
"entry_fill_identity_sha256": entry_fill_identity,
942+
"stop_entry_fill_identity_sha256": stop_entry_fill_identity,
943+
}
944+
945+
strategy_breaker = losses is not None and losses >= 5
946+
account_breaker = account_drawdown is not None and account_drawdown > 0.10
947+
if strategy_breaker:
948+
errors.add("strategy_breaker_triggered")
949+
if account_breaker:
950+
errors.add("account_breaker_triggered")
951+
952+
payload = {
953+
"as_of": _utc_timestamp(as_of) if as_of is not None else None,
954+
"mandate_id": risk_control_state.get("mandate_id"),
955+
"candidate_identity_sha256": _sha256(
956+
risk_control_state.get("candidate_identity_sha256")
957+
),
958+
"stop_loss_distance": stop_loss_distance,
959+
"stop_fill_policy": risk_control_state.get("stop_fill_policy"),
960+
"position_stop_states": normalized_stop_states,
961+
"consecutive_completed_losing_exits": losses,
962+
"account_drawdown_fraction": account_drawdown,
963+
"drawdown_scalar": drawdown_scalar,
964+
}
965+
return {
966+
"stop_loss_distance": stop_loss_distance,
967+
"stop_intent_ready": all_stop_intents_ready,
968+
"strategy_breaker_triggered": strategy_breaker,
969+
"account_breaker_triggered": account_breaker,
970+
"account_drawdown_fraction": account_drawdown,
971+
"drawdown_scalar": drawdown_scalar,
972+
"risk_control_state_digest_sha256": _canonical_digest(payload),
973+
}, errors
974+
975+
679976
def assess_with_evidence(
680977
decision: StrategyDecision,
681978
portfolio_snapshot: Any,
@@ -733,8 +1030,13 @@ def assess_with_evidence(
7331030
weighted_exposure = 0.0
7341031
if mandate_provenance is None and len(active_positions) > 1:
7351032
reason_codes.add("fallback_position_count")
1033+
mandate_id = mandate.get("mandate_id")
1034+
exact_research_mandate = mandate_id in {
1035+
_TQQQ_ETF_ONLY_RESEARCH_MANDATE,
1036+
_GLOBAL_ETF_RESEARCH_MANDATE,
1037+
}
7361038
if (
737-
mandate.get("mandate_id") == _TQQQ_ETF_ONLY_RESEARCH_MANDATE
1039+
exact_research_mandate
7381040
and len(active_positions) > mandate["max_nonzero_assets"]
7391041
):
7401042
reason_codes.add("single_strategy_position_count")
@@ -777,6 +1079,22 @@ def assess_with_evidence(
7771079
):
7781080
reason_codes.add("risk_budget_exposure_cap")
7791081
weighted_exposure += weight * factor
1082+
if mandate_id == _GLOBAL_ETF_RESEARCH_MANDATE:
1083+
stop_distance = control_fields["stop_loss_distance"]
1084+
drawdown_scalar = control_fields["drawdown_scalar"]
1085+
loss_budget = mandate.get("loss_budget")
1086+
modeled_stop_loss = (
1087+
sum(weight for _symbol, weight in active_positions) * stop_distance
1088+
if stop_distance is not None
1089+
else None
1090+
)
1091+
if (
1092+
modeled_stop_loss is not None
1093+
and drawdown_scalar is not None
1094+
and loss_budget is not None
1095+
and modeled_stop_loss > loss_budget * drawdown_scalar + 1e-9
1096+
):
1097+
reason_codes.add("risk_budget_exposure_cap")
7801098
target_weights: dict[str, float] = {}
7811099
for symbol, weight in active_positions:
7821100
target_weights[symbol] = target_weights.get(symbol, 0.0) + weight
@@ -788,9 +1106,7 @@ def assess_with_evidence(
7881106
product_leverage_factors=factors,
7891107
effective_exposure_cap=cap,
7901108
observed_effective_exposure=observed,
791-
cash_only=(
792-
mandate.get("mandate_id") == _TQQQ_ETF_ONLY_RESEARCH_MANDATE
793-
),
1109+
cash_only=exact_research_mandate,
7941110
)
7951111
if not valid_normalization:
7961112
reason_codes.add("invalid_reduce_only_normalization")

0 commit comments

Comments
 (0)