Cancel remaining entries before fetching break-even fees

After the first take-profit fills, cancel outstanding entry orders before calculating remaining-position protection. Missing fee records still fail explicitly and leave existing stops in place without allowing the old entry to remain open.

Cover complete and incomplete fees on Binance and Bybit, including cancellation before the recovery fetch and no duplicate cancellation. Verify the staged snapshot with 61 trading safety and plan tests.
This commit is contained in:
eezd committed 2026-10-26 14:23:51 +08:00
1 parent d1a6325ed7
commit 641351a002
2 files changed
+44

No files matched your search

@@ -574,6 +574,11 @@ class Executor:
for j in range(len(TAKE_PROFIT_STEPS[sizing["direction"]]))]
stop = Decimal(sizing["entry"]["stop"])
if tp_filled[0] > 0 and position is not None:
# 撤销剩余入场不依赖手续费;先撤,避免保本计算失败后仍可能继续加仓。
for row in active:
if row.purpose == "entry":
await self.cancel(client, row)
active = [r for r in active if r.purpose != "entry"]
fee_per_unit = await self.entry_fee_per_unit(session, client, plan.symbol, entry_rows)
break_even = remaining_break_even(sizing["direction"], average, fee_per_unit,
ESTIMATED_CLOSE_FEE_RATES[client.exchange], rules.tick_size)
+39
View File
@@ -172,6 +172,7 @@ def test_missing_entry_fees_leave_existing_protection_untouched(trading, monkeyp
engine, client = trading.Executor(), Client()
p, entry, old = plan(trading), order(trading, "entry"), order(trading, "stop")
entry.exchange_id, entry.filled_qty, entry.avg_price = "entry-id", Decimal(1), Decimal(100)
entry.status = "filled"
entry.created_at = datetime.now(UTC)
tp = trading.TradingOrder(purpose="tp", leg=0, filled_qty=Decimal("0.6"), status="filled")
monkeypatch.setattr(client, "trades", AsyncMock(return_value=[]), raising=False)
@@ -187,6 +188,44 @@ def test_missing_entry_fees_leave_existing_protection_untouched(trading, monkeyp
asyncio.run(check())
@pytest.mark.parametrize("exchange", ["binance", "bybit"])
@pytest.mark.parametrize("fees_complete", [False, True])
def test_tp1_cancels_remaining_entry_before_fee_lookup(trading, monkeypatch, exchange, fees_complete):
async def check():
engine, client = trading.Executor(), Client()
client.exchange = engine.exchange = exchange
p, entry, old = plan(trading), order(trading, "entry"), order(trading, "stop")
p.plan_status = "open"
entry.exchange_id, entry.filled_qty, entry.avg_price = "entry-id", Decimal("0.5"), Decimal(100)
entry.status, entry.created_at = "partially_filled", datetime.now(UTC)
old.trigger_price = Decimal(90)
tp = trading.TradingOrder(purpose="tp", leg=0, filled_qty=Decimal("0.3"), status="filled")
fill = SimpleNamespace(order_id="entry-id", qty=entry.filled_qty, commission_asset="USDT", commission=Decimal("0.01"))
session = Session(groups=[[fill]] if fees_complete else [[], []])
position = trading.Position("long", Decimal("0.2"), Decimal(100), Decimal(101))
async def trades(*args):
# 补取仍缺手续费时,撤入场单也必须先完成。
assert client.canceled == ["ap2"]
return []
monkeypatch.setattr(client, "trades", AsyncMock(side_effect=trades), raising=False)
monkeypatch.setattr(trading, "save_fills", AsyncMock())
try:
if fees_complete:
await engine.sync_orders(session, client, p, [entry, old, tp], position, True, RULES, False)
else:
with pytest.raises(ExchangeError, match="手续费尚未完整同步"):
await engine.sync_orders(session, client, p, [entry, old, tp], position, True, RULES, False)
assert client.created == []
assert client.canceled == ["ap2"]
assert client.canceled.count("ap2") == 1
assert all(spec.purpose != "entry" for spec in client.created)
finally:
await engine.market.http.aclose()
asyncio.run(check())
@pytest.mark.parametrize("asset", ["USDT", "BNB"])
def test_entry_fees_recover_complete_partial_fills_without_using_realized_profit(trading, monkeypatch, asset):
async def check():