fix(trading): close 3 race / lifecycle bugs in adopt-then-release path
1. recovery.py rehydrated adopted trades with no System-2 ladder ───────────────────────────────────────────────────────────────── On backend restart, recovery resolved stop_ladder / derisk_ladder / addon_ladder / peak_trail from trigger_post.category. Adopted trades have trigger_post_id=NULL (the user opened on HL manually, there's no source post) so all four resolved to None and the trade restarted as a vanilla "stop_loss + max_hold" position — the entire System-2 ladder logic silently disappeared across any restart. Fix: detect System-2 from the row itself (sys2_mode IS NOT NULL falls back to the adopted-category constant), then rebuild the full ladder set from frozen leverage + mode. derisk_steps_done / addon_steps_done / peak_gain_pct on the row let us pick up exactly where we left off. 2. close_and_finalize raced with release_management ───────────────────────────────────────────────────────────────── on_price_tick snapshots _watched, evaluates per trade, appends to a triggered list, then spawns _fire_close as an async task. If release_management runs between the snapshot and _fire_close firing, close_and_finalize would still close the HL position the user just took back manual control of. Fix: atomic claim now requires released_at IS NULL by default. New `force=True` parameter on close_and_finalize bypasses the guard for the explicit user-close API (manual_close passes it) so a deliberate user click still works on a released trade. 3. partial_derisk + pyramid_add raced with release_management ───────────────────────────────────────────────────────────────── Same race shape: a price tick captured before release could still trigger a partial reduce-only close, or pyramid INTO a position the user is now driving themselves. Fix: both functions return idempotent-success if released_at IS NOT NULL — match the closed_at handling we already had. Tests still 64/64 + preflight + smoke all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -316,6 +316,9 @@ async def manual_close(
|
||||
# close_and_finalize handles BOTH paper and live branches internally.
|
||||
from app.services.bot_engine import close_and_finalize
|
||||
leverage = trade.leverage if trade.leverage is not None else sub.leverage
|
||||
# force=True so an explicit user close still works on a released trade
|
||||
# (the released_at guard exists to stop AUTOMATED paths from racing
|
||||
# past a release; a manual click is the user being explicit).
|
||||
await close_and_finalize(
|
||||
trade_id=trade.id,
|
||||
api_key=api_key,
|
||||
@@ -323,6 +326,7 @@ async def manual_close(
|
||||
asset=trade.asset,
|
||||
wallet=wallet,
|
||||
reason="manual",
|
||||
force=True,
|
||||
)
|
||||
|
||||
closed = (await db.execute(select(BotTrade).where(BotTrade.id == trade_id))).scalar_one()
|
||||
|
||||
Reference in New Issue
Block a user