Skip to content

Commit e1d14cb

Browse files
Pigbibicodex
andauthored
fix(execution): serialize live cycles with persistent state owner (#212)
Co-authored-by: Codex <noreply@openai.com>
1 parent be014fa commit e1d14cb

12 files changed

Lines changed: 550 additions & 99 deletions

application/cycle_service.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from quant_platform_kit.strategy_lifecycle.performance_monitor import try_record_platform_execution
1010
from application.execution_receipt_adapter import attach_execution_receipt_from_report
1111
from runtime_logging import RuntimeLogContext, emit_runtime_log
12-
from runtime_support import append_report_error, finalize_notification_delivery
12+
from runtime_support import (
13+
append_report_error, finalize_notification_delivery, acquire_runtime_state_owner,
14+
release_runtime_state_owner, reconcile_runtime_cash_effects, ExecutionIntegrityError,
15+
)
1316

1417

1518
def execute_strategy_cycle(
@@ -58,7 +61,11 @@ def execute_strategy_cycle(
5861
"Runtime target disables standard execution; monitoring continues and all order/state-write calls are suppressed."
5962
)
6063

64+
state_healthy = False
6165
try:
66+
if not acquire_runtime_state_owner(runtime):
67+
report["execution_blocked_reason"] = "state_owner_busy"
68+
return report
6269
if not ensure_runtime_client(runtime, report):
6370
return report
6471

@@ -67,6 +74,9 @@ def execute_strategy_cycle(
6774
return report
6875

6976
state, trend_pool_resolution, runtime_trend_universe, allow_new_trend_entries = cycle_state
77+
runtime.trade_state = state
78+
if state.get("order_submission", {}).get("state") == "SUBMISSION_UNKNOWN":
79+
raise ExecutionIntegrityError("order_reconciliation_uncertain")
7080
append_trend_pool_source_logs(log_buffer, trend_pool_resolution, allow_new_trend_entries)
7181

7282
report["upstream_pool_symbols"] = list(runtime_trend_universe.keys())
@@ -87,6 +97,7 @@ def execute_strategy_cycle(
8797
btc_snapshot = market_snapshot["btc_snapshot"]
8898
trend_indicators = market_snapshot["trend_indicators"]
8999

100+
state_healthy = True
90101
allocation = compute_portfolio_allocation(
91102
runtime,
92103
runtime_trend_universe,
@@ -155,6 +166,11 @@ def execute_strategy_cycle(
155166
)
156167
if fuel_status != "ready":
157168
report["execution_blocked_reason"] = f"bnb_fuel_{fuel_status}"
169+
if fuel_status == "filled_pending_snapshot":
170+
reconcile_runtime_cash_effects(runtime, state)
171+
runtime_set_trade_state(runtime, report, state, reason="cash_reconciliation")
172+
else:
173+
state_healthy = False
158174
return report
159175

160176
u_total = execute_trend_rotation(
@@ -243,16 +259,24 @@ def execute_strategy_cycle(
243259
)
244260

245261
state["last_balance_snapshot"] = build_balance_snapshot(runtime_trend_universe, balances, u_total)
262+
reconcile_runtime_cash_effects(runtime, state)
246263
runtime_set_trade_state(runtime, report, state, reason="cycle_complete")
247264

248265
except Exception:
266+
state_healthy = False
249267
report["status"] = "error"
250268
append_report_error(report, "cycle_execution_failed", stage="execute_cycle")
251269
try:
252270
runtime_notify(runtime, report, f"{translate_fn('system_crash')}\ncycle_execution_failed")
253271
except Exception:
254272
pass
255273
finally:
274+
if state_healthy and getattr(runtime, "state_owner_held", False):
275+
try:
276+
release_runtime_state_owner(runtime)
277+
except ExecutionIntegrityError:
278+
report["status"] = "error"
279+
append_report_error(report, "state_owner_release_uncertain", stage="state_release")
256280
report["log_lines"] = list(log_buffer)
257281
finalize_notification_delivery(report)
258282
attach_execution_receipt_from_report(report)

infra/binance_runtime.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from math import isfinite
6+
57
from runtime_support import ExecutionIntegrityError
68

79

@@ -92,23 +94,22 @@ def ensure_asset_available_runtime(
9294
method_name="redeem_simple_earn_flexible_product",
9395
payload={"productId": product_id, "amount": redeem_amt},
9496
effect_type="earn_redeem",
97+
accounting_asset=asset,
9598
)
9699
append_log_fn(
97100
log_buffer,
98101
translate_fn("execution_spot_short_redeeming_from_earn", asset=asset, amount=redeem_amt),
99102
)
100103
if not runtime.dry_run:
101104
sleep_fn(3)
105+
observed_free = float(runtime.client.get_asset_balance(asset=asset)["free"])
106+
if not isfinite(observed_free) or observed_free + 1e-8 < spot_free + redeem_amt:
107+
raise ExecutionIntegrityError("earn_reconciliation_pending")
102108
return True
103109
except ExecutionIntegrityError:
104110
raise
105111
except Exception:
106-
runtime_notify_fn(
107-
runtime,
108-
report,
109-
f"{translate_fn('redeem_failed')} {asset}\n"
110-
f"{translate_fn('error_label')}: asset_availability_failed",
111-
)
112+
raise ExecutionIntegrityError("asset_availability_failed") from None
112113
return False
113114

114115

@@ -153,7 +154,12 @@ def manage_usdt_earn_buffer_runtime(
153154
method_name="subscribe_simple_earn_flexible_product",
154155
payload={"productId": product_id, "amount": excess},
155156
effect_type="earn_subscribe",
157+
accounting_asset=asset,
156158
)
159+
if not runtime.dry_run:
160+
observed_free = float(runtime.client.get_asset_balance(asset=asset)["free"])
161+
if not isfinite(observed_free) or observed_free < 0 or observed_free > spot_free - excess + 1e-8:
162+
raise ExecutionIntegrityError("earn_reconciliation_pending")
157163
append_log_fn(log_buffer, translate_fn("cash_manager_subscribed_to_earn", amount=excess))
158164
elif spot_free < target_buffer - 5.0:
159165
shortfall = round(target_buffer - spot_free, 4)
@@ -177,15 +183,17 @@ def manage_usdt_earn_buffer_runtime(
177183
method_name="redeem_simple_earn_flexible_product",
178184
payload={"productId": product_id, "amount": redeem_amt},
179185
effect_type="earn_redeem",
186+
accounting_asset=asset,
180187
)
188+
if not runtime.dry_run:
189+
observed_free = float(runtime.client.get_asset_balance(asset=asset)["free"])
190+
if not isfinite(observed_free) or observed_free + 1e-8 < spot_free + redeem_amt:
191+
raise ExecutionIntegrityError("earn_reconciliation_pending")
181192
append_log_fn(log_buffer, translate_fn("cash_manager_redeeming_to_spot", amount=redeem_amt))
182193
except ExecutionIntegrityError:
183194
raise
184195
except Exception:
185-
append_log_fn(
186-
log_buffer,
187-
translate_fn("usdt_earn_buffer_maintenance_failed", error="earn_buffer_maintenance_failed"),
188-
)
196+
raise ExecutionIntegrityError("earn_buffer_maintenance_failed") from None
189197

190198

191199
def ensure_runtime_client(

live_services.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ def get_state_doc_ref(*, collection="strategy", document="MULTI_ASSET_STATE"):
2626
return get_firestore_client().collection(collection).document(document)
2727

2828

29-
def load_trade_state(*, normalize_fn, default_state_factory, normalize=True, collection="strategy", document="MULTI_ASSET_STATE"):
29+
def load_trade_state(*, normalize_fn, default_state_factory, normalize=True, collection="strategy", document="MULTI_ASSET_STATE", store=None):
3030
try:
31-
payload = _get_document_store().get(collection=collection, document_id=document)
31+
payload = (store if store is not None else _get_document_store()).get(collection=collection, document_id=document)
3232
if payload is not None:
3333
return normalize_fn(payload) if normalize else payload
3434
return default_state_factory() if normalize else {}
@@ -37,16 +37,60 @@ def load_trade_state(*, normalize_fn, default_state_factory, normalize=True, col
3737
return None
3838

3939

40-
def save_trade_state(data, *, normalize_fn, collection="strategy", document="MULTI_ASSET_STATE"):
40+
def save_trade_state(data, *, normalize_fn, collection="strategy", document="MULTI_ASSET_STATE", store=None):
4141
try:
4242
persisted_state = normalize_fn(data)
43-
_get_document_store().set(collection=collection, document_id=document, data=persisted_state)
43+
(store if store is not None else _get_document_store()).set(collection=collection, document_id=document, data=persisted_state)
4444
return True
4545
except Exception:
4646
print(t("firestore_write_failed", error="state_persistence_failed"))
4747
return False
4848

4949

50+
def bind_trade_state_access(*, normalize_fn, default_state_factory,
51+
collection="strategy", document="MULTI_ASSET_STATE"):
52+
"""Bind this runtime's state and persistent owner to the same Firestore backend."""
53+
store = _get_document_store()
54+
55+
def load(normalize=True):
56+
return load_trade_state(normalize_fn=normalize_fn, default_state_factory=default_state_factory,
57+
normalize=normalize, collection=collection, document=document, store=store)
58+
59+
def save(data):
60+
return save_trade_state(data, normalize_fn=normalize_fn, collection=collection, document=document, store=store)
61+
62+
def owner_document():
63+
return store.client.collection(collection).document(document + "__owner")
64+
65+
def claim(owner_id):
66+
from google.api_core.exceptions import AlreadyExists
67+
if not isinstance(owner_id, str) or not owner_id.strip():
68+
raise ValueError("state_owner_required")
69+
try:
70+
owner_document().create({"owner_id": owner_id}, retry=None)
71+
except AlreadyExists:
72+
return False
73+
return True
74+
75+
def release(owner_id):
76+
from google.cloud import firestore
77+
if not isinstance(owner_id, str) or not owner_id.strip():
78+
raise ValueError("state_owner_required")
79+
ref = owner_document()
80+
81+
@firestore.transactional
82+
def delete_owned(transaction):
83+
snapshot = ref.get(transaction=transaction, retry=None)
84+
if not snapshot.exists or snapshot.to_dict().get("owner_id") != owner_id:
85+
return False
86+
transaction.delete(ref)
87+
return True
88+
89+
return delete_owned(store.client.transaction(max_attempts=1))
90+
91+
return load, save, claim, release
92+
93+
5094
def send_tg_msg(token, chat_id, text):
5195
message = build_telegram_message(text)
5296
receipt = {

main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
manage_usdt_earn_buffer as qpk_manage_usdt_earn_buffer,
3030
)
3131
from live_services import (
32+
bind_trade_state_access,
3233
get_firestore_client as live_get_firestore_client,
3334
get_state_doc_ref as live_get_state_doc_ref,
3435
send_tg_msg as live_send_tg_msg,
@@ -766,6 +767,12 @@ def build_live_runtime(now_utc=None):
766767
state_writer=set_trade_state,
767768
notifier=lambda **kwargs: send_tg_msg(kwargs["token"], kwargs["chat_id"], kwargs["text"]),
768769
)
770+
if not runtime.dry_run and runtime.standard_execution_permitted:
771+
runtime.state_loader, runtime.state_writer, runtime.state_owner_claim, runtime.state_owner_release = bind_trade_state_access(
772+
normalize_fn=normalize_trade_state, default_state_factory=build_default_state,
773+
)
774+
runtime.fuel_symbol = BNB_FUEL_SYMBOL
775+
runtime.fuel_asset = BNB_FUEL_ASSET
769776
_activate_execution_strategy_runtime(runtime.strategy_profile)
770777
return runtime
771778

0 commit comments

Comments
 (0)