diff --git a/application/cycle_service.py b/application/cycle_service.py index 05746ca2..70d3a11d 100644 --- a/application/cycle_service.py +++ b/application/cycle_service.py @@ -15,6 +15,7 @@ try_record_platform_execution, ) from application.execution_receipt_adapter import attach_execution_receipt_from_report +from application.execution_kernel_adapter import consult_t2_unknown_new_cycle from application.portfolio_service import EARN_FORWARD_REASON_CODES from runtime_logging import RuntimeLogContext, emit_runtime_log from runtime_support import ( @@ -231,6 +232,8 @@ def execute_strategy_cycle( reconcile_pending_funding_submission(runtime) submission_state = state.get("order_submission", {}).get("state", "RESERVED") if submission_state == "SUBMISSION_UNKNOWN": + # N13: redundant T2 consult; local raise always wins (no behavior change). + consult_t2_unknown_new_cycle() raise ExecutionIntegrityError("order_reconciliation_uncertain") if submission_state == "FILLED_ACCOUNTING_PENDING": raise ExecutionIntegrityError("filled_order_accounting_unverifiable") diff --git a/application/execution_kernel_adapter.py b/application/execution_kernel_adapter.py new file mode 100644 index 00000000..4d181f2f --- /dev/null +++ b/application/execution_kernel_adapter.py @@ -0,0 +1,49 @@ +"""Thin adapter: map Binance SUBMISSION_UNKNOWN deny paths onto QPK execution_kernel T2/T3. + +N13: consult-only / redundant fail-closed. Local raise still always wins. +Does not own claims or change allow / reconcile-success paths. +Does not touch FILLED_ACCOUNTING_PENDING (B13-b G3; BN-only). +""" + +from __future__ import annotations + +from quant_platform_kit.execution_kernel import ( + ExecutionGuardDecision, + SubmissionCertainty, + SubmissionRetrySnapshot, + SubmissionUnknownSnapshot, + deny_blind_retry_after_uncertain_transport, + deny_new_cycle_when_submission_unknown, +) + + +def consult_t2_unknown_new_cycle() -> ExecutionGuardDecision: + """Consult T2 when cycle entry finds SUBMISSION_UNKNOWN and will raise. + + Caller must still raise ExecutionIntegrityError("order_reconciliation_uncertain"). + """ + return deny_new_cycle_when_submission_unknown( + SubmissionUnknownSnapshot( + submission_certainty=SubmissionCertainty.UNKNOWN, + requesting_new_cycle_submit=True, + ) + ) + + +def consult_t3_deny_blind_retry( + *, + transport_uncertain: bool = True, + reconciled: bool = False, +) -> ExecutionGuardDecision: + """Consult T3 on paths that already refuse blind retry after uncertain transport. + + Caller must still raise OrderReconciliationError("order_reconciliation_uncertain"). + Do not call on reconcile-success return paths. + """ + return deny_blind_retry_after_uncertain_transport( + SubmissionRetrySnapshot( + transport_uncertain=bool(transport_uncertain), + reconciled=bool(reconciled), + requesting_blind_retry=True, + ) + ) diff --git a/pyproject.toml b/pyproject.toml index d99245e5..8ad4d9e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "QuantStrategyLab platform layer for Binance exchange." requires-python = ">=3.11" dependencies = [ - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8e8ec51884bf8abb0a7ca699fc2da39fad134f89", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@155d74a615ccdebf7223787939c2dc7151d75efd", "crypto-strategies @ git+https://github.com/QuantStrategyLab/CryptoStrategies.git@7fcca8ee0280b3e66245eb673caadda8b8b3b9a3", "python-binance", "pandas", @@ -23,7 +23,7 @@ test = [ [tool.uv] override-dependencies = [ - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8e8ec51884bf8abb0a7ca699fc2da39fad134f89", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@155d74a615ccdebf7223787939c2dc7151d75efd", ] [tool.ruff] diff --git a/qsl.toml b/qsl.toml index d2c29498..06cb3c23 100644 --- a/qsl.toml +++ b/qsl.toml @@ -9,7 +9,7 @@ expires_at = "2026-09-30" next_action = "keep uv.lock current and maintain QPK/CryptoStrategies pin consistency" [qsl.requires] -quant_platform_kit = "8e8ec51884bf8abb0a7ca699fc2da39fad134f89" +quant_platform_kit = "155d74a615ccdebf7223787939c2dc7151d75efd" crypto_strategies = "7fcca8ee0280b3e66245eb673caadda8b8b3b9a3" [qsl.compat] diff --git a/runtime_support.py b/runtime_support.py index 8d17fcf1..1054146e 100644 --- a/runtime_support.py +++ b/runtime_support.py @@ -13,6 +13,7 @@ import requests from quant_platform_kit.common.runtime_reports import build_runtime_report_base +from application.execution_kernel_adapter import consult_t3_deny_blind_retry from application.execution_receipt_adapter import ( record_order_failure, record_order_response, @@ -1210,6 +1211,8 @@ def runtime_call_client(runtime, report, *, method_name, payload, effect_type, submission_status = submission_record["state"] if submission_status == _ORDER_SUBMISSION_UNKNOWN: if "method_name" in submission_record or is_earn_call: + # N13: redundant T3 consult; local raise always wins (no behavior change). + consult_t3_deny_blind_retry(transport_uncertain=True, reconciled=False) raise OrderReconciliationError("order_reconciliation_uncertain") from None current_payload, current_identity_sha256 = _ensure_order_logical_identity(runtime, method_name, payload) if current_identity_sha256 != submission_record["identity_sha256"]: @@ -1300,6 +1303,8 @@ def runtime_call_client(runtime, report, *, method_name, payload, effect_type, payload={"payload": dict(client_payload), "reason": "order_reconciliation_uncertain", "retries": 0}, executed=False, ) + # N13: redundant T3 consult; local raise always wins (no behavior change). + consult_t3_deny_blind_retry(transport_uncertain=True, reconciled=False) raise OrderReconciliationError("order_reconciliation_uncertain") from None if not _is_confirmed_earn_success(method_name, response): record_side_effect( @@ -1310,6 +1315,8 @@ def runtime_call_client(runtime, report, *, method_name, payload, effect_type, payload={"payload": dict(client_payload), "reason": "order_reconciliation_uncertain", "retries": 0}, executed=False, ) + # N13: redundant T3 consult; local raise always wins (no behavior change). + consult_t3_deny_blind_retry(transport_uncertain=True, reconciled=False) raise OrderReconciliationError("order_reconciliation_uncertain") from None record_side_effect( runtime, diff --git a/tests/test_execution_kernel_adapter.py b/tests/test_execution_kernel_adapter.py new file mode 100644 index 00000000..df133fcf --- /dev/null +++ b/tests/test_execution_kernel_adapter.py @@ -0,0 +1,40 @@ +"""N13: Binance adapter around QPK execution_kernel T2/T3 (UNKNOWN deny paths).""" + +from __future__ import annotations + +import unittest + +from application.execution_kernel_adapter import ( + consult_t2_unknown_new_cycle, + consult_t3_deny_blind_retry, +) + + +class ExecutionKernelAdapterTests(unittest.TestCase): + def test_t2_unknown_new_cycle_denies(self): + decision = consult_t2_unknown_new_cycle() + self.assertEqual(decision.decision, "deny") + self.assertEqual(decision.reason_code, "submission_uncertain") + self.assertFalse(decision.allowed) + + def test_t3_uncertain_unreconciled_blind_retry_denies(self): + decision = consult_t3_deny_blind_retry( + transport_uncertain=True, + reconciled=False, + ) + self.assertEqual(decision.decision, "deny") + self.assertEqual(decision.reason_code, "reconcile_required") + self.assertFalse(decision.allowed) + + def test_t3_reconciled_allows_kernel(self): + """If already reconciled, T3 allows; callers must not request blind retry here.""" + decision = consult_t3_deny_blind_retry( + transport_uncertain=True, + reconciled=True, + ) + self.assertEqual(decision.decision, "allow") + self.assertTrue(decision.allowed) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index c3ef1c05..1b8fb5d4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -17,7 +17,7 @@ resolution-markers = [ ] [manifest] -overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=8e8ec51884bf8abb0a7ca699fc2da39fad134f89" }] +overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=155d74a615ccdebf7223787939c2dc7151d75efd" }] [[package]] name = "aiohappyeyeballs" @@ -214,7 +214,7 @@ requires-dist = [ { name = "pandas" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8" }, { name = "python-binance" }, - { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=8e8ec51884bf8abb0a7ca699fc2da39fad134f89" }, + { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=155d74a615ccdebf7223787939c2dc7151d75efd" }, { name = "requests" }, { name = "ruff", marker = "extra == 'test'", specifier = ">=0.12" }, ] @@ -1658,7 +1658,7 @@ wheels = [ [[package]] name = "quant-platform-kit" version = "1.0.0" -source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=8e8ec51884bf8abb0a7ca699fc2da39fad134f89#8e8ec51884bf8abb0a7ca699fc2da39fad134f89" } +source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=155d74a615ccdebf7223787939c2dc7151d75efd#155d74a615ccdebf7223787939c2dc7151d75efd" } [[package]] name = "regex"