-
Notifications
You must be signed in to change notification settings - Fork 0
fix: bind Binance execution authority to QPK 9618 #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,101 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from collections.abc import Mapping | ||
| from datetime import datetime, timezone | ||
| from typing import Any | ||
|
|
||
| from quant_platform_kit.risk.contracts import CandidateRiskIdentity | ||
| from quant_platform_kit.risk.gate import ( | ||
| _FALLBACK_MAX_SNAPSHOT_AGE_SECONDS_V1, | ||
| _canonical_digest, | ||
| _decision_metrics, | ||
| _parse_utc_timestamp, | ||
| ) | ||
| from quant_platform_kit.strategy_contracts import StrategyDecision | ||
|
|
||
|
|
||
| _SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") | ||
|
|
||
|
|
||
| def _approved_scoped_assessment( | ||
| value: Any, | ||
| *, | ||
| scope: str, | ||
| now: datetime, | ||
| ) -> Mapping[str, Any] | None: | ||
| """Accept only serialized QPK approval evidence; never infer or recompute risk authority.""" | ||
| if not isinstance(value, Mapping): | ||
| return None | ||
| reason_codes = value.get("reason_codes") | ||
| if ( | ||
| value.get("scope") != scope | ||
| or value.get("outcome") != "APPROVE" | ||
| or not isinstance(reason_codes, (list, tuple)) | ||
| or reason_codes | ||
| ): | ||
| return None | ||
| for field in ( | ||
| "mandate_authority_receipt_sha256", | ||
| "candidate_identity_sha256", | ||
| "decision_digest_sha256", | ||
| "portfolio_snapshot_digest_sha256", | ||
| "assessment_sha256", | ||
| ): | ||
| if not isinstance(value.get(field), str) or not _SHA256_PATTERN.fullmatch(value[field]): | ||
|
Comment on lines
+38
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When serialized assessment evidence is altered after QPK produced it, this check accepts any 64-character Useful? React with 👍 / 👎. |
||
| return None | ||
| for field in ("contract_version", "evaluated_at", "policy_id", "policy_version"): | ||
| if not isinstance(value.get(field), str) or not value[field].strip(): | ||
|
Pigbibi marked this conversation as resolved.
|
||
| return None | ||
| evaluated_at = _parse_utc_timestamp(value["evaluated_at"]) | ||
| if evaluated_at is None: | ||
| return None | ||
| age_seconds = (now - evaluated_at).total_seconds() | ||
| if not 0.0 <= age_seconds <= _FALLBACK_MAX_SNAPSHOT_AGE_SECONDS_V1: | ||
| return None | ||
| return value | ||
|
|
||
|
|
||
| def has_execution_authority(decision: StrategyDecision | None) -> bool: | ||
| """Require matching QPK RiskEngine, MEMBER, and ACCOUNT approval evidence.""" | ||
| if not isinstance(decision, StrategyDecision): | ||
| return False | ||
| diagnostics = decision.diagnostics | ||
| if not isinstance(diagnostics, Mapping) or diagnostics.get("risk_gate") != "APPROVE": | ||
| return False | ||
| if any(str(flag).startswith("rejected:") for flag in decision.risk_flags): | ||
| return False | ||
| now = datetime.now(timezone.utc) | ||
| member = _approved_scoped_assessment( | ||
| diagnostics.get("member_risk_assessment"), | ||
| scope="MEMBER", | ||
| now=now, | ||
| ) | ||
| account = _approved_scoped_assessment( | ||
| diagnostics.get("account_risk_assessment"), | ||
| scope="ACCOUNT", | ||
| now=now, | ||
| ) | ||
| if member is None or account is None: | ||
| return False | ||
| candidate_identity = diagnostics.get("candidate_risk_identity") | ||
| if not isinstance(candidate_identity, CandidateRiskIdentity): | ||
| return False | ||
| try: | ||
| decision_payload, _, _ = _decision_metrics(decision, total_equity=None) | ||
| decision_digest = _canonical_digest(decision_payload) | ||
| except (TypeError, ValueError): | ||
| return False | ||
| return ( | ||
| member["candidate_identity_sha256"] | ||
| == account["candidate_identity_sha256"] | ||
| == candidate_identity.candidate_sha256 | ||
| and member["decision_digest_sha256"] | ||
| == account["decision_digest_sha256"] | ||
| == decision_digest | ||
|
Comment on lines
+90
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When MEMBER and ACCOUNT approvals were produced from different portfolio snapshots within the five-minute freshness window, this comparison still grants authority as long as the candidate and decision digests match. Because Useful? React with 👍 / 👎. |
||
| ) | ||
|
|
||
|
|
||
| def _budget_map(decision: StrategyDecision) -> dict[str, float]: | ||
| values: dict[str, float] = {} | ||
| for budget in decision.budgets: | ||
|
|
@@ -28,28 +118,40 @@ def map_strategy_decision_to_allocation( | |
| account_metrics: Mapping[str, Any], | ||
| ) -> dict[str, float]: | ||
| diagnostics = dict(decision.diagnostics) | ||
| budgets = _budget_map(decision) | ||
| positions = _position_weight_map(decision) | ||
| trend_target_ratio = float( | ||
| diagnostics.get( | ||
| "trend_target_ratio", | ||
| sum(weight for symbol, weight in positions.items() if symbol != "BTCUSDT"), | ||
| authorized = has_execution_authority(decision) | ||
| budgets = _budget_map(decision) if authorized else {} | ||
| positions = _position_weight_map(decision) if authorized else {} | ||
| trend_target_ratio = ( | ||
| float( | ||
| diagnostics.get( | ||
| "trend_target_ratio", | ||
| sum(weight for symbol, weight in positions.items() if symbol != "BTCUSDT"), | ||
| ) | ||
| ) | ||
| if authorized | ||
| else 0.0 | ||
| ) | ||
| return { | ||
| "total_equity": float(account_metrics["total_equity"]), | ||
| "trend_val": float(account_metrics["trend_value"]), | ||
| "dca_val": float(account_metrics["dca_value"]), | ||
| "btc_target_ratio": float(diagnostics.get("btc_target_ratio", positions.get("BTCUSDT", 0.0))), | ||
| "btc_target_ratio": ( | ||
| float(diagnostics.get("btc_target_ratio", positions.get("BTCUSDT", 0.0))) | ||
| if authorized | ||
| else 0.0 | ||
| ), | ||
| "trend_target_ratio": trend_target_ratio, | ||
| "trend_usdt_pool": float(budgets.get("trend_rotation_pool", 0.0)), | ||
| "dca_usdt_pool": float(budgets.get("btc_core_dca_pool", 0.0)), | ||
| "btc_base_order_usdt": float(diagnostics.get("btc_base_order_usdt", 0.0)), | ||
| "btc_base_order_usdt": ( | ||
| float(diagnostics.get("btc_base_order_usdt", 0.0)) if authorized else 0.0 | ||
| ), | ||
| } | ||
|
|
||
|
|
||
| def map_strategy_decision_to_rotation_plan(decision: StrategyDecision) -> dict[str, Any]: | ||
| diagnostics = dict(decision.diagnostics) | ||
| authorized = has_execution_authority(decision) | ||
| metadata = diagnostics.get("metadata") if isinstance(diagnostics.get("metadata"), Mapping) else {} | ||
| combo_meta = metadata.get("combo") if isinstance(metadata.get("combo"), Mapping) else {} | ||
| selected_candidates = { | ||
|
|
@@ -59,20 +161,24 @@ def map_strategy_decision_to_rotation_plan(decision: StrategyDecision) -> dict[s | |
| "abs_momentum": float(payload.get("abs_momentum", 0.0)), | ||
| } | ||
| for symbol, payload in dict(diagnostics.get("rotation_candidates", {})).items() | ||
| } | ||
| } if authorized else {} | ||
| planned_trend_buys = { | ||
| str(symbol): float(amount) | ||
| for symbol, amount in dict(diagnostics.get("planned_trend_buys", {})).items() | ||
| } | ||
| } if authorized else {} | ||
| sell_reasons = { | ||
| str(symbol): str(reason) | ||
| for symbol, reason in dict(diagnostics.get("sell_reasons", {})).items() | ||
| if str(reason) | ||
| } | ||
| } if authorized else {} | ||
| return { | ||
| "active_trend_pool": list(diagnostics.get("trend_pool", ())), | ||
| "selected_candidates": selected_candidates, | ||
| "eligible_buy_symbols": [str(symbol) for symbol in diagnostics.get("eligible_buy_symbols", ())], | ||
| "eligible_buy_symbols": ( | ||
| [str(symbol) for symbol in diagnostics.get("eligible_buy_symbols", ())] | ||
| if authorized | ||
| else [] | ||
| ), | ||
| "planned_trend_buys": planned_trend_buys, | ||
| "sell_reasons": sell_reasons, | ||
| "rotation_pool_source_version": diagnostics.get("rotation_pool_source_version"), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever execution authority is approved, this second capture replaces
prices,balances, andu_total, butallocation,total_equity, andtrend_val_equitystill come from the first snapshot. If prices move while the two snapshots and strategy evaluation run—or the BNB top-up changes balances—the portfolio report, daily PnL, state rebasing, and circuit-breaker decision use the old valuation while subsequent execution uses the new market data. Recompute the allocation from the replacement snapshot, or isolate the fuel top-up without replacing the inputs used downstream.Useful? React with 👍 / 👎.