fix: validate historical trade chain changes
This commit is contained in:
@@ -143,6 +143,15 @@ class TradingService:
|
|||||||
return self.save_trade(trade)
|
return self.save_trade(trade)
|
||||||
|
|
||||||
def delete_trade(self, trade_id: int) -> None:
|
def delete_trade(self, trade_id: int) -> None:
|
||||||
|
trade = self.repository.get_trade(trade_id)
|
||||||
|
if trade is None:
|
||||||
|
return
|
||||||
|
remaining_trades = [
|
||||||
|
existing
|
||||||
|
for existing in self.repository.list_trades(account_id=trade.account_id)
|
||||||
|
if existing.id != trade_id
|
||||||
|
]
|
||||||
|
self._validate_trade_chain(remaining_trades)
|
||||||
self.repository.delete_trade(trade_id)
|
self.repository.delete_trade(trade_id)
|
||||||
|
|
||||||
def list_trades(self, *, instrument_id: int | None = None) -> list[Trade]:
|
def list_trades(self, *, instrument_id: int | None = None) -> list[Trade]:
|
||||||
@@ -216,15 +225,21 @@ class TradingService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _validate_resulting_position(self, trade: Trade, existing_trades: list[Trade]) -> None:
|
def _validate_resulting_position(self, trade: Trade, existing_trades: list[Trade]) -> None:
|
||||||
|
self._validate_trade_chain([*existing_trades, trade])
|
||||||
|
|
||||||
|
def _validate_trade_chain(self, trades: list[Trade]) -> None:
|
||||||
|
if not trades:
|
||||||
|
return
|
||||||
instruments = self.repository.list_instruments()
|
instruments = self.repository.list_instruments()
|
||||||
|
as_of = max(item.trade_date for item in trades)
|
||||||
try:
|
try:
|
||||||
calculate_positions(
|
calculate_positions(
|
||||||
instruments,
|
instruments,
|
||||||
[*existing_trades, trade],
|
trades,
|
||||||
as_of=trade.trade_date,
|
as_of=as_of,
|
||||||
)
|
)
|
||||||
except CalculationError as exc:
|
except CalculationError as exc:
|
||||||
raise ValueError(str(exc)) from exc
|
raise ValueError(f"后续成交重算失败:{exc}") from exc
|
||||||
|
|
||||||
def _instrument_cashflows(self, account_id: int) -> dict[int, Decimal]:
|
def _instrument_cashflows(self, account_id: int) -> dict[int, Decimal]:
|
||||||
totals: dict[int, Decimal] = defaultdict(lambda: Decimal("0"))
|
totals: dict[int, Decimal] = defaultdict(lambda: Decimal("0"))
|
||||||
|
|||||||
@@ -129,3 +129,51 @@ def test_service_validates_lot_size_and_available_sell_quantity(tmp_path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert saved_sell.id is not None
|
assert saved_sell.id is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_rejects_historical_changes_that_break_future_sells(tmp_path):
|
||||||
|
service = TradingService(tmp_path / "grid.db")
|
||||||
|
service.ensure_defaults()
|
||||||
|
account = service.get_active_account()
|
||||||
|
instrument = service.add_instrument(Instrument(id=None, code="159915", name="创业板ETF"))
|
||||||
|
buy = service.save_trade(
|
||||||
|
Trade(
|
||||||
|
id=None,
|
||||||
|
account_id=account.id,
|
||||||
|
instrument_id=instrument.id,
|
||||||
|
trade_date=date(2026, 7, 7),
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price=Decimal("2.00"),
|
||||||
|
quantity=200,
|
||||||
|
trade_group=TradeGroup.GRID,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
service.save_trade(
|
||||||
|
Trade(
|
||||||
|
id=None,
|
||||||
|
account_id=account.id,
|
||||||
|
instrument_id=instrument.id,
|
||||||
|
trade_date=date(2026, 7, 8),
|
||||||
|
side=TradeSide.SELL,
|
||||||
|
price=Decimal("2.10"),
|
||||||
|
quantity=200,
|
||||||
|
trade_group=TradeGroup.GRID,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="后续成交"):
|
||||||
|
service.update_trade(
|
||||||
|
Trade(
|
||||||
|
id=buy.id,
|
||||||
|
account_id=account.id,
|
||||||
|
instrument_id=instrument.id,
|
||||||
|
trade_date=date(2026, 7, 7),
|
||||||
|
side=TradeSide.BUY,
|
||||||
|
price=Decimal("2.00"),
|
||||||
|
quantity=100,
|
||||||
|
trade_group=TradeGroup.GRID,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="后续成交"):
|
||||||
|
service.delete_trade(buy.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user